-
Notifications
You must be signed in to change notification settings - Fork 121
feat(laboratory): scope environment variables to target #6500
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 5 commits
dcdb72e
47ee32f
ce95ba6
2ef5394
c166d97
c7b974c
f4bdfe6
b7729cc
a83b7a3
b92f941
84b7d52
895d220
8a1c77f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
--- | ||
'hive': minor | ||
--- | ||
|
||
Laboratory Environment Variables are now scoped to Target. | ||
|
||
Previously, we stored environment variables as a static key in your browser's local storage. This meant that any changes to the environment variables would affect all targets' Laboratory. | ||
|
||
Now when you use Laboratory, any changes to the environment variables will not affect the environment variables of other targets' Laboratory. | ||
|
||
## Migration Details (TL;DR: You Won't Notice Anything!) | ||
|
||
For an indefinite period of time we will support the following migration when you load Laboratory on any target. If this holds true: | ||
|
||
1. Your browser's localStorage has a key for the global environment variables; | ||
2. Your browser's localStorage does NOT have a key for scoped environment variables for the Target Laboratory being loaded; | ||
|
||
Then we will initialize the scoped environment variables for the Target Laboratory being loaded with the global ones. | ||
|
||
Laboratory will _never_ write to the global environment variables again, so this should give you a seamless migration to scoped environment variables for all your targets. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { | ||
createKeyValueStoreMemory, | ||
KeyValueStoreDatabase, | ||
PreviousEntriesPolicy, | ||
readVersionedEntry, | ||
VersionedEntrySpec, | ||
} from './versioned-entry'; | ||
|
||
interface TestCase { | ||
databaseBefore: KeyValueStoreDatabase; | ||
databaseAfter: KeyValueStoreDatabase; | ||
spec: VersionedEntrySpec; | ||
value: string | null; | ||
previousEntriesPolicy?: PreviousEntriesPolicy; | ||
} | ||
|
||
const a = 'a'; | ||
const b = 'b'; | ||
const c = 'c'; | ||
|
||
// prettier-ignore | ||
test.for<TestCase>([ | ||
// Returns null if spec key is missing in db | ||
{ spec: [{ key:a }], databaseBefore: {}, databaseAfter: {}, value: null }, | ||
{ spec: [{ key:a }], databaseBefore: {b}, databaseAfter: {b}, value: null }, | ||
// Returns value if spec key is present in db | ||
{ spec: [{ key:a }], databaseBefore: {a}, databaseAfter: {a}, value: a }, | ||
{ spec: [{ key:a }], databaseBefore: {a,b}, databaseAfter: {a,b}, value: a }, | ||
{ spec: [{ key:a }, {key:b}], databaseBefore: {a}, databaseAfter: {a}, value: a }, | ||
// | ||
// With previousEntriesPolicy = ignore (default) | ||
// | ||
// Previous spec keys are NOT removed from db | ||
{ spec: [{ key:a }, {key:b}], databaseBefore: {a,b}, databaseAfter: {a,b}, value: a }, | ||
{ spec: [{ key:a }, {key:b}, {key:c}], databaseBefore: {a,b,c}, databaseAfter: {a,b,c}, value: a }, | ||
// Latest found spec key is returned | ||
{ spec: [{ key:a }, {key:b}], databaseBefore: {b}, databaseAfter: {a:b,b}, value: b }, | ||
{ spec: [{ key:a }, {key:b}, {key:c}], databaseBefore: {c}, databaseAfter: {a:c,c}, value: c }, | ||
{ spec: [{ key:a }, {key:b}, {key:c}], databaseBefore: {b,c}, databaseAfter: {a:b,b,c}, value: b }, | ||
// | ||
// With previousEntriesPolicy = remove | ||
// | ||
// Previous spec keys are removed from db | ||
{ spec: [{ key:a }, {key:b}], databaseBefore: {a,b}, databaseAfter: {a}, value: a, previousEntriesPolicy: 'remove' }, | ||
{ spec: [{ key:a }, {key:b}, {key:c}], databaseBefore: {a,b,c}, databaseAfter: {a}, value: a, previousEntriesPolicy: 'remove' }, | ||
// Latest found spec key is returned AND removed from db if not current spec | ||
{ spec: [{ key:a }, {key:b}], databaseBefore: {b}, databaseAfter: {a:b}, value: b, previousEntriesPolicy: 'remove' }, | ||
{ spec: [{ key:a }, {key:b}, {key:c}], databaseBefore: {c}, databaseAfter: {a:c}, value: c, previousEntriesPolicy: 'remove' }, | ||
{ spec: [{ key:a }, {key:b}, {key:c}], databaseBefore: {b,c}, databaseAfter: {a:b}, value: b, previousEntriesPolicy: 'remove' }, | ||
// Non-spec keys in db are not removed | ||
{ spec: [{ key:a }, {key:b}], databaseBefore: {a,b,c}, databaseAfter: {a,c}, value: a, previousEntriesPolicy: 'remove' }, | ||
])( | ||
'%j', | ||
({ databaseBefore, databaseAfter, spec, value, previousEntriesPolicy }) => { | ||
const readVersionedEntryMemory = readVersionedEntry(createKeyValueStoreMemory(databaseBefore)); | ||
const valueActual = readVersionedEntryMemory({spec, previousEntriesPolicy}) | ||
expect(databaseBefore).toEqual(databaseAfter) | ||
expect(valueActual).toEqual(value) | ||
}, | ||
); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
// -------------------------------------------------------------------- | ||
// KeyValueStore Interface | ||
// -------------------------------------------------------------------- | ||
|
||
export interface KeyValueStore { | ||
get(key: string): string | null; | ||
set(key: string, value: string): void; | ||
remove(key: string): void; | ||
} | ||
|
||
export type KeyValueStoreDatabase = Record<string, string>; | ||
|
||
// -------------------------------------------------------------------- | ||
// Versioned Entry Data Types | ||
// -------------------------------------------------------------------- | ||
|
||
export type VersionedEntrySpec = readonly [EntrySpec, ...(readonly EntrySpec[])]; | ||
|
||
interface EntrySpec { | ||
key: string; | ||
// todo once we have use-case | ||
// schema: | ||
// fromPrevious: | ||
} | ||
|
||
// -------------------------------------------------------------------- | ||
// Versioned Entry Functions | ||
// -------------------------------------------------------------------- | ||
|
||
/** | ||
* Read a versioned entry from local storage. | ||
* | ||
* Migrations are automatically applied to bring previous entries up to date with current. | ||
* | ||
* 1. The latest entry value is returned. | ||
* 2. If the latest entry to have a value is NOT the current entry, then current entry is set to the latest value. | ||
* 3. All entries prior the current that are present are either deleted or ignored based on removalStrategy. | ||
* | ||
* @param options.removalStrategy - Strategy for handling previous entries (RemovalStrategy.Remove or RemovalStrategy.Ignore, defaults to Ignore) | ||
*/ | ||
export const readVersionedEntry = | ||
(keyValueStore: KeyValueStore) => | ||
(parameters: { | ||
spec: VersionedEntrySpec; | ||
/** | ||
* @defaultValue 'ignore' | ||
*/ | ||
previousEntriesPolicy?: PreviousEntriesPolicy; | ||
}): string | null => { | ||
type SearchResult = SearchResultHit | SearchResultMiss; | ||
|
||
interface SearchResultHit extends SearchResultEither { | ||
value: string; | ||
} | ||
|
||
interface SearchResultMiss extends SearchResultEither { | ||
value: null; | ||
} | ||
|
||
interface SearchResultEither { | ||
value: string | null; | ||
entry: EntrySpec; | ||
index: number; | ||
} | ||
|
||
// --- | ||
const { spec, previousEntriesPolicy = PreviousEntriesPolicy.ignore } = parameters; | ||
|
||
const searchResults: SearchResult[] = []; | ||
|
||
for (const { entry, index } of spec.map((entry, index) => ({ entry, index }))) { | ||
const value = keyValueStore.get(entry.key); | ||
searchResults.push({ entry, value, index }); | ||
// Note: Once we have schemas, we should not remove here, wait until _after_ successful migration | ||
if (index > 0 && previousEntriesPolicy === PreviousEntriesPolicy.remove) { | ||
keyValueStore.remove(entry.key); | ||
} | ||
} | ||
Comment on lines
+76
to
+83
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainConsider deferring removal until after successful migration. The code removes previous entries before potential schema migration, as noted in the comment. This could lead to data loss if migration fails. 🏁 Script executed: #!/bin/bash
# Search for any schema migration implementations or tests
rg -g '!node_modules' -A 5 'schema.*migration|migration.*schema' Length of output: 14789 Action: Defer Removal Until Post-Migration Success The current implementation in Recommendations:
|
||
|
||
const latestHit = searchResults.find(({ value }) => value !== null) as | ||
| SearchResultHit | ||
| undefined; | ||
|
||
if (!latestHit) return null; | ||
|
||
if (latestHit.index > 0) { | ||
keyValueStore.set(spec[0].key, latestHit.value); | ||
// Note: Once we have schemas, we will need to run the value through the migration pipeline. | ||
} | ||
|
||
return latestHit.value; | ||
}; | ||
|
||
export const PreviousEntriesPolicy = { | ||
remove: 'remove', | ||
ignore: 'ignore', | ||
} as const; | ||
|
||
export type PreviousEntriesPolicy = keyof typeof PreviousEntriesPolicy; | ||
|
||
// -------------------------------------------------------------------- | ||
// KeyValueStore Implementations | ||
// -------------------------------------------------------------------- | ||
|
||
export const keyValueStoreLocalStorage: KeyValueStore = { | ||
get(key) { | ||
return localStorage.getItem(key); | ||
}, | ||
set(key, value) { | ||
localStorage.setItem(key, value); | ||
}, | ||
remove(key) { | ||
localStorage.removeItem(key); | ||
}, | ||
}; | ||
|
||
export const readVersionedEntryLocalStorage = readVersionedEntry(keyValueStoreLocalStorage); | ||
|
||
export const createKeyValueStoreMemory = (database: KeyValueStoreDatabase): KeyValueStore => ({ | ||
get(key) { | ||
return database[key] ?? null; | ||
}, | ||
set(key, value) { | ||
database[key] = value; | ||
}, | ||
remove(key) { | ||
delete database[key]; | ||
}, | ||
}); |
Uh oh!
There was an error while loading. Please reload this page.