@inlang/sdk 0.35.4 → 0.35.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter/solidAdapter.test.js +1 -1
- package/dist/api.d.ts +2 -13
- package/dist/api.d.ts.map +1 -1
- package/dist/createMessagesQuery.d.ts.map +1 -1
- package/dist/createMessagesQuery.js +68 -82
- package/dist/createNewProject.test.js +1 -3
- package/dist/loadProject.d.ts.map +1 -1
- package/dist/loadProject.js +67 -32
- package/dist/loadProject.test.js +6 -2
- package/dist/persistence/batchedIO.d.ts +11 -0
- package/dist/persistence/batchedIO.d.ts.map +1 -0
- package/dist/persistence/batchedIO.js +49 -0
- package/dist/persistence/batchedIO.test.d.ts +2 -0
- package/dist/persistence/batchedIO.test.d.ts.map +1 -0
- package/dist/persistence/batchedIO.test.js +56 -0
- package/dist/persistence/filelock/acquireFileLock.d.ts.map +1 -1
- package/dist/persistence/filelock/acquireFileLock.js +3 -1
- package/dist/persistence/filelock/releaseLock.d.ts.map +1 -1
- package/dist/persistence/filelock/releaseLock.js +2 -1
- package/dist/persistence/store.d.ts +107 -0
- package/dist/persistence/store.d.ts.map +1 -0
- package/dist/persistence/store.js +99 -0
- package/dist/persistence/store.test.d.ts +2 -0
- package/dist/persistence/store.test.d.ts.map +1 -0
- package/dist/persistence/store.test.js +79 -0
- package/dist/persistence/storeApi.d.ts +22 -0
- package/dist/persistence/storeApi.d.ts.map +1 -0
- package/dist/persistence/storeApi.js +1 -0
- package/dist/reactivity/solid.test.js +1 -6
- package/dist/resolve-modules/plugins/resolvePlugins.d.ts.map +1 -1
- package/dist/resolve-modules/plugins/resolvePlugins.js +3 -10
- package/dist/test-utilities/sleep.d.ts +4 -0
- package/dist/test-utilities/sleep.d.ts.map +1 -0
- package/dist/test-utilities/sleep.js +9 -0
- package/dist/v2/helper.d.ts +131 -0
- package/dist/v2/helper.d.ts.map +1 -0
- package/dist/v2/helper.js +75 -0
- package/dist/v2/helper.test.d.ts +2 -0
- package/dist/v2/helper.test.d.ts.map +1 -0
- package/dist/v2/{createMessageBundle.test.js → helper.test.js} +1 -1
- package/dist/v2/index.d.ts +2 -0
- package/dist/v2/index.d.ts.map +1 -1
- package/dist/v2/index.js +2 -1
- package/dist/v2/mocks/index.d.ts +3 -0
- package/dist/v2/mocks/index.d.ts.map +1 -0
- package/dist/v2/mocks/index.js +2 -0
- package/dist/v2/mocks/multipleMatcher/bundle.d.ts +3 -0
- package/dist/v2/mocks/multipleMatcher/bundle.d.ts.map +1 -0
- package/dist/v2/mocks/multipleMatcher/bundle.js +194 -0
- package/dist/v2/mocks/multipleMatcher/bundle.test.d.ts +2 -0
- package/dist/v2/mocks/multipleMatcher/bundle.test.d.ts.map +1 -0
- package/dist/v2/mocks/multipleMatcher/bundle.test.js +10 -0
- package/dist/v2/mocks/plural/bundle.d.ts +1 -1
- package/dist/v2/mocks/plural/bundle.d.ts.map +1 -1
- package/dist/v2/mocks/plural/bundle.js +1 -1
- package/dist/v2/mocks/plural/bundle.test.js +6 -6
- package/dist/v2/shim.d.ts +12 -0
- package/dist/v2/shim.d.ts.map +1 -0
- package/dist/v2/shim.js +151 -0
- package/dist/v2/shim.test.d.ts +2 -0
- package/dist/v2/shim.test.d.ts.map +1 -0
- package/dist/v2/shim.test.js +49 -0
- package/dist/v2/stubQueryApi.d.ts +9 -0
- package/dist/v2/stubQueryApi.d.ts.map +1 -0
- package/dist/v2/stubQueryApi.js +38 -0
- package/dist/v2/types.d.ts +110 -0
- package/dist/v2/types.d.ts.map +1 -1
- package/dist/v2/types.js +9 -0
- package/package.json +9 -8
- package/src/adapter/solidAdapter.test.ts +1 -1
- package/src/api.ts +2 -13
- package/src/createMessagesQuery.ts +80 -99
- package/src/createNewProject.test.ts +1 -4
- package/src/loadProject.test.ts +6 -2
- package/src/loadProject.ts +86 -45
- package/src/persistence/batchedIO.test.ts +63 -0
- package/src/persistence/batchedIO.ts +64 -0
- package/src/persistence/filelock/acquireFileLock.ts +5 -2
- package/src/persistence/filelock/releaseLock.ts +2 -1
- package/src/persistence/store.test.ts +102 -0
- package/src/persistence/store.ts +119 -0
- package/src/persistence/storeApi.ts +19 -0
- package/src/reactivity/solid.test.ts +1 -8
- package/src/resolve-modules/plugins/resolvePlugins.ts +4 -13
- package/src/test-utilities/sleep.ts +11 -0
- package/src/v2/{createMessageBundle.test.ts → helper.test.ts} +1 -1
- package/src/v2/helper.ts +98 -0
- package/src/v2/index.ts +2 -0
- package/src/v2/mocks/index.ts +2 -0
- package/src/v2/mocks/multipleMatcher/bundle.test.ts +11 -0
- package/src/v2/mocks/multipleMatcher/bundle.ts +196 -0
- package/src/v2/mocks/plural/bundle.test.ts +6 -6
- package/src/v2/mocks/plural/bundle.ts +1 -1
- package/src/v2/shim.test.ts +56 -0
- package/src/v2/shim.ts +173 -0
- package/src/v2/stubQueryApi.ts +43 -0
- package/src/v2/types.ts +17 -0
- package/dist/persistence/plugin.d.ts +0 -31
- package/dist/persistence/plugin.d.ts.map +0 -1
- package/dist/persistence/plugin.js +0 -42
- package/dist/persistence/plugin.test.d.ts +0 -2
- package/dist/persistence/plugin.test.d.ts.map +0 -1
- package/dist/persistence/plugin.test.js +0 -49
- package/dist/v2/createMessageBundle.d.ts +0 -25
- package/dist/v2/createMessageBundle.d.ts.map +0 -1
- package/dist/v2/createMessageBundle.js +0 -36
- package/dist/v2/createMessageBundle.test.d.ts +0 -2
- package/dist/v2/createMessageBundle.test.d.ts.map +0 -1
- package/src/persistence/plugin.test.ts +0 -60
- package/src/persistence/plugin.ts +0 -56
- package/src/v2/createMessageBundle.ts +0 -43
package/dist/v2/types.js
CHANGED
|
@@ -67,3 +67,12 @@ export const MessageBundle = Type.Object({
|
|
|
67
67
|
alias: Type.Record(Type.String(), Type.String()),
|
|
68
68
|
messages: Type.Array(Message),
|
|
69
69
|
});
|
|
70
|
+
export const MessageSlot = Type.Object({
|
|
71
|
+
locale: LanguageTag,
|
|
72
|
+
slot: Type.Literal(true),
|
|
73
|
+
});
|
|
74
|
+
export const MessageBundleWithSlots = Type.Object({
|
|
75
|
+
id: Type.String(),
|
|
76
|
+
alias: Type.Record(Type.String(), Type.String()),
|
|
77
|
+
messages: Type.Array(Type.Union([Message, MessageSlot])),
|
|
78
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inlang/sdk",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.35.
|
|
4
|
+
"version": "0.35.6",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"./test-utilities": "./dist/test-utilities/index.js",
|
|
17
17
|
"./lint": "./dist/lint/index.js",
|
|
18
18
|
"./messages": "./dist/messages/index.js",
|
|
19
|
-
"./v2": "./dist/v2/index.js"
|
|
19
|
+
"./v2": "./dist/v2/index.js",
|
|
20
|
+
"./v2-mocks": "./dist/v2/mocks/index.js"
|
|
20
21
|
},
|
|
21
22
|
"files": [
|
|
22
23
|
"./dist",
|
|
@@ -34,16 +35,16 @@
|
|
|
34
35
|
"solid-js": "1.6.12",
|
|
35
36
|
"throttle-debounce": "^5.0.0",
|
|
36
37
|
"@inlang/json-types": "1.1.0",
|
|
37
|
-
"@inlang/message": "2.1.0",
|
|
38
|
-
"@inlang/module": "1.2.13",
|
|
39
38
|
"@inlang/language-tag": "1.5.1",
|
|
39
|
+
"@inlang/module": "1.2.13",
|
|
40
|
+
"@inlang/message": "2.1.0",
|
|
40
41
|
"@inlang/plugin": "2.4.13",
|
|
41
|
-
"@inlang/
|
|
42
|
+
"@inlang/project-settings": "2.4.2",
|
|
42
43
|
"@inlang/message-lint-rule": "1.4.7",
|
|
44
|
+
"@inlang/result": "1.1.0",
|
|
45
|
+
"@lix-js/fs": "2.1.0",
|
|
43
46
|
"@inlang/translatable": "1.3.1",
|
|
44
|
-
"@lix-js/client": "2.2.0"
|
|
45
|
-
"@inlang/project-settings": "2.4.2",
|
|
46
|
-
"@lix-js/fs": "2.1.0"
|
|
47
|
+
"@lix-js/client": "2.2.0"
|
|
47
48
|
},
|
|
48
49
|
"devDependencies": {
|
|
49
50
|
"@types/debug": "^4.1.12",
|
|
@@ -222,7 +222,7 @@ describe("messages", () => {
|
|
|
222
222
|
// TODO: how can we await `setConfig` correctly
|
|
223
223
|
await new Promise((resolve) => setTimeout(resolve, 510))
|
|
224
224
|
|
|
225
|
-
expect(effectOnMessagesCounter).toBe(
|
|
225
|
+
expect(effectOnMessagesCounter).toBe(2) // 2 = setSetting (clearing the message index), subsequencial loadMessage call
|
|
226
226
|
expect(Object.values(project.query.messages.getAll()).length).toBe(2)
|
|
227
227
|
})
|
|
228
228
|
|
package/src/api.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
MessageLintReport,
|
|
11
11
|
} from "./versionedInterfaces.js"
|
|
12
12
|
import type { ResolvedPluginApi } from "./resolve-modules/plugins/types.js"
|
|
13
|
-
import type
|
|
13
|
+
import type { StoreApi } from "./persistence/storeApi.js"
|
|
14
14
|
|
|
15
15
|
export type InstalledPlugin = {
|
|
16
16
|
id: Plugin["id"]
|
|
@@ -58,18 +58,7 @@ export type InlangProject = {
|
|
|
58
58
|
}
|
|
59
59
|
// WIP V2 message apis
|
|
60
60
|
// use with project settings: experimental.persistence = true
|
|
61
|
-
|
|
62
|
-
messages?: Query<V2.Message>
|
|
63
|
-
variants?: Query<V2.Variant>
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* WIP template for async V2 crud interfaces
|
|
68
|
-
* E.g. `await project.messageBundles.get({ id: "..." })`
|
|
69
|
-
**/
|
|
70
|
-
interface Query<T> {
|
|
71
|
-
get: (args: unknown) => Promise<T>
|
|
72
|
-
getAll: () => Promise<T[]>
|
|
61
|
+
store?: StoreApi
|
|
73
62
|
}
|
|
74
63
|
|
|
75
64
|
// const x = {} as InlangProject
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Message } from "@inlang/message"
|
|
2
2
|
import { ReactiveMap } from "./reactivity/map.js"
|
|
3
|
-
import { createEffect, onCleanup } from "./reactivity/solid.js"
|
|
3
|
+
import { createEffect, onCleanup, batch } from "./reactivity/solid.js"
|
|
4
4
|
import { createSubscribable } from "./loadProject.js"
|
|
5
5
|
import type { InlangProject, MessageQueryApi, MessageQueryDelegate } from "./api.js"
|
|
6
6
|
import type { ResolvedPluginApi } from "./resolve-modules/plugins/types.js"
|
|
@@ -16,10 +16,6 @@ import { PluginLoadMessagesError, PluginSaveMessagesError } from "./errors.js"
|
|
|
16
16
|
import { humanIdHash } from "./storage/human-id/human-readable-id.js"
|
|
17
17
|
const debug = _debug("sdk:messages")
|
|
18
18
|
|
|
19
|
-
function sleep(ms: number) {
|
|
20
|
-
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
21
|
-
}
|
|
22
|
-
|
|
23
19
|
type MessageState = {
|
|
24
20
|
messageDirtyFlags: {
|
|
25
21
|
[messageId: string]: boolean
|
|
@@ -278,8 +274,6 @@ export function createMessagesQuery({
|
|
|
278
274
|
// - saving a message in two different languages would lead to a write in de.json first
|
|
279
275
|
// - This will leads to a load of the messages and since en.json has not been saved yet the english variant in the message would get overritten with the old state again
|
|
280
276
|
|
|
281
|
-
const maxMessagesPerTick = 500
|
|
282
|
-
|
|
283
277
|
/**
|
|
284
278
|
* Messsage that loads messages from a plugin - this method synchronizes with the saveMessage funciton.
|
|
285
279
|
* If a save is in progress loading will wait until saving is done. If another load kicks in during this load it will queue the
|
|
@@ -326,104 +320,91 @@ async function loadMessagesViaPlugin(
|
|
|
326
320
|
})
|
|
327
321
|
)
|
|
328
322
|
|
|
329
|
-
let loadedMessageCount = 0
|
|
330
|
-
|
|
331
323
|
const deletedMessages = new Set(messages.keys())
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- length has checked beforhand
|
|
349
|
-
deletedMessages.delete(currentMessages[0]!.id)
|
|
350
|
-
// update message in place - leave message id and alias untouched
|
|
351
|
-
loadedMessageClone.alias = {} as any
|
|
352
|
-
|
|
353
|
-
// TODO #1585 we have to map the id of the importedMessage to the alias and fill the id property with the id of the existing message - change when import mesage provides importedMessage.alias
|
|
354
|
-
if (experimentalAliases) {
|
|
355
|
-
loadedMessageClone.alias["default"] = loadedMessageClone.id
|
|
324
|
+
batch(() => {
|
|
325
|
+
for (const loadedMessage of loadedMessages) {
|
|
326
|
+
const loadedMessageClone = structuredClone(loadedMessage)
|
|
327
|
+
|
|
328
|
+
const currentMessages = [...messages.values()]
|
|
329
|
+
// TODO #1585 here we match using the id to support legacy load message plugins - after we introduced import / export methods we will use importedMessage.alias
|
|
330
|
+
.filter(
|
|
331
|
+
(message: any) =>
|
|
332
|
+
(experimentalAliases ? message.alias["default"] : message.id) === loadedMessage.id
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
if (currentMessages.length > 1) {
|
|
336
|
+
// NOTE: if we happen to find two messages witht the sam alias we throw for now
|
|
337
|
+
// - this could be the case if one edits the aliase manualy
|
|
338
|
+
throw new Error("more than one message with the same id or alias found ")
|
|
339
|
+
} else if (currentMessages.length === 1) {
|
|
356
340
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- length has checked beforhand
|
|
357
|
-
|
|
341
|
+
deletedMessages.delete(currentMessages[0]!.id)
|
|
342
|
+
// update message in place - leave message id and alias untouched
|
|
343
|
+
loadedMessageClone.alias = {} as any
|
|
344
|
+
|
|
345
|
+
// TODO #1585 we have to map the id of the importedMessage to the alias and fill the id property with the id of the existing message - change when import mesage provides importedMessage.alias
|
|
346
|
+
if (experimentalAliases) {
|
|
347
|
+
loadedMessageClone.alias["default"] = loadedMessageClone.id
|
|
348
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- length has checked beforhand
|
|
349
|
+
loadedMessageClone.id = currentMessages[0]!.id
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// NOTE stringifyMessage encodes messages independent from key order!
|
|
353
|
+
const importedEnecoded = stringifyMessage(loadedMessageClone)
|
|
354
|
+
|
|
355
|
+
// NOTE could use hash instead of the whole object JSON to save memory...
|
|
356
|
+
if (messageState.messageLoadHash[loadedMessageClone.id] === importedEnecoded) {
|
|
357
|
+
// debug("skipping upsert!")
|
|
358
|
+
continue
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// This logic is preventing cycles - could also be handled if update api had a parameter for who triggered update
|
|
362
|
+
// e.g. when FS was updated, we don't need to write back to FS
|
|
363
|
+
// update is synchronous, so update effect will be triggered immediately
|
|
364
|
+
// NOTE: this might trigger a save before we have the chance to delete - but since save is async and waits for the lock acquired by this method - its save to set the flags afterwards
|
|
365
|
+
messages.set(loadedMessageClone.id, loadedMessageClone)
|
|
366
|
+
// NOTE could use hash instead of the whole object JSON to save memory...
|
|
367
|
+
messageState.messageLoadHash[loadedMessageClone.id] = importedEnecoded
|
|
368
|
+
delegate?.onMessageUpdate(loadedMessageClone.id, loadedMessageClone, [
|
|
369
|
+
...messages.values(),
|
|
370
|
+
])
|
|
371
|
+
} else {
|
|
372
|
+
// message with the given alias does not exist so far
|
|
373
|
+
loadedMessageClone.alias = {} as any
|
|
374
|
+
// TODO #1585 we have to map the id of the importedMessage to the alias - change when import mesage provides importedMessage.alias
|
|
375
|
+
if (experimentalAliases) {
|
|
376
|
+
loadedMessageClone.alias["default"] = loadedMessageClone.id
|
|
377
|
+
|
|
378
|
+
let currentOffset = 0
|
|
379
|
+
let messsageId: string | undefined
|
|
380
|
+
do {
|
|
381
|
+
messsageId = humanIdHash(loadedMessageClone.id, currentOffset)
|
|
382
|
+
if (messages.get(messsageId)) {
|
|
383
|
+
currentOffset += 1
|
|
384
|
+
messsageId = undefined
|
|
385
|
+
}
|
|
386
|
+
} while (messsageId === undefined)
|
|
387
|
+
|
|
388
|
+
// create a humanId based on a hash of the alias
|
|
389
|
+
loadedMessageClone.id = messsageId
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const importedEnecoded = stringifyMessage(loadedMessageClone)
|
|
393
|
+
|
|
394
|
+
// we don't have to check - done before hand if (messages.has(loadedMessageClone.id)) return false
|
|
395
|
+
messages.set(loadedMessageClone.id, loadedMessageClone)
|
|
396
|
+
messageState.messageLoadHash[loadedMessageClone.id] = importedEnecoded
|
|
397
|
+
delegate?.onMessageUpdate(loadedMessageClone.id, loadedMessageClone, [
|
|
398
|
+
...messages.values(),
|
|
399
|
+
])
|
|
358
400
|
}
|
|
359
|
-
|
|
360
|
-
// NOTE stringifyMessage encodes messages independent from key order!
|
|
361
|
-
const importedEnecoded = stringifyMessage(loadedMessageClone)
|
|
362
|
-
|
|
363
|
-
// NOTE could use hash instead of the whole object JSON to save memory...
|
|
364
|
-
if (messageState.messageLoadHash[loadedMessageClone.id] === importedEnecoded) {
|
|
365
|
-
// debug("skipping upsert!")
|
|
366
|
-
continue
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// This logic is preventing cycles - could also be handled if update api had a parameter for who triggered update
|
|
370
|
-
// e.g. when FS was updated, we don't need to write back to FS
|
|
371
|
-
// update is synchronous, so update effect will be triggered immediately
|
|
372
|
-
// NOTE: this might trigger a save before we have the chance to delete - but since save is async and waits for the lock acquired by this method - its save to set the flags afterwards
|
|
373
|
-
messages.set(loadedMessageClone.id, loadedMessageClone)
|
|
374
|
-
// NOTE could use hash instead of the whole object JSON to save memory...
|
|
375
|
-
messageState.messageLoadHash[loadedMessageClone.id] = importedEnecoded
|
|
376
|
-
delegate?.onMessageUpdate(loadedMessageClone.id, loadedMessageClone, [...messages.values()])
|
|
377
|
-
loadedMessageCount++
|
|
378
|
-
} else {
|
|
379
|
-
// message with the given alias does not exist so far
|
|
380
|
-
loadedMessageClone.alias = {} as any
|
|
381
|
-
// TODO #1585 we have to map the id of the importedMessage to the alias - change when import mesage provides importedMessage.alias
|
|
382
|
-
if (experimentalAliases) {
|
|
383
|
-
loadedMessageClone.alias["default"] = loadedMessageClone.id
|
|
384
|
-
|
|
385
|
-
let currentOffset = 0
|
|
386
|
-
let messsageId: string | undefined
|
|
387
|
-
do {
|
|
388
|
-
messsageId = humanIdHash(loadedMessageClone.id, currentOffset)
|
|
389
|
-
if (messages.get(messsageId)) {
|
|
390
|
-
currentOffset += 1
|
|
391
|
-
messsageId = undefined
|
|
392
|
-
}
|
|
393
|
-
} while (messsageId === undefined)
|
|
394
|
-
|
|
395
|
-
// create a humanId based on a hash of the alias
|
|
396
|
-
loadedMessageClone.id = messsageId
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
const importedEnecoded = stringifyMessage(loadedMessageClone)
|
|
400
|
-
|
|
401
|
-
// we don't have to check - done before hand if (messages.has(loadedMessageClone.id)) return false
|
|
402
|
-
messages.set(loadedMessageClone.id, loadedMessageClone)
|
|
403
|
-
messageState.messageLoadHash[loadedMessageClone.id] = importedEnecoded
|
|
404
|
-
delegate?.onMessageUpdate(loadedMessageClone.id, loadedMessageClone, [...messages.values()])
|
|
405
|
-
loadedMessageCount++
|
|
406
401
|
}
|
|
407
|
-
if (loadedMessageCount > maxMessagesPerTick) {
|
|
408
|
-
// move loading of the next messages to the next ticks to allow solid to cleanup resources
|
|
409
|
-
// solid needs some time to settle and clean up
|
|
410
|
-
// https://github.com/solidjs-community/solid-primitives/blob/9ca76a47ffa2172770e075a90695cf933da0ff48/packages/trigger/src/index.ts#L64
|
|
411
|
-
await sleep(0)
|
|
412
|
-
loadedMessageCount = 0
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
402
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
delegate?.onMessageDelete(deletedMessageId, [...messages.values()])
|
|
420
|
-
loadedMessageCount++
|
|
421
|
-
if (loadedMessageCount > maxMessagesPerTick) {
|
|
422
|
-
await sleep(0)
|
|
423
|
-
loadedMessageCount = 0
|
|
403
|
+
for (const deletedMessageId of deletedMessages) {
|
|
404
|
+
messages.delete(deletedMessageId)
|
|
405
|
+
delegate?.onMessageDelete(deletedMessageId, [...messages.values()])
|
|
424
406
|
}
|
|
425
|
-
}
|
|
426
|
-
|
|
407
|
+
})
|
|
427
408
|
await releaseLock(fs as NodeishFilesystem, lockDirPath, "loadMessage", lockTime)
|
|
428
409
|
lockTime = undefined
|
|
429
410
|
|
|
@@ -4,10 +4,7 @@ import { mockRepo } from "@lix-js/client"
|
|
|
4
4
|
import { defaultProjectSettings } from "./defaultProjectSettings.js"
|
|
5
5
|
import { loadProject } from "./loadProject.js"
|
|
6
6
|
import { createMessage } from "./test-utilities/createMessage.js"
|
|
7
|
-
|
|
8
|
-
function sleep(ms: number) {
|
|
9
|
-
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
10
|
-
}
|
|
7
|
+
import { sleep } from "./test-utilities/sleep.js"
|
|
11
8
|
|
|
12
9
|
describe("createNewProject", () => {
|
|
13
10
|
it("should throw if a path does not end with .inlang", async () => {
|
package/src/loadProject.test.ts
CHANGED
|
@@ -278,11 +278,15 @@ describe("initialization", () => {
|
|
|
278
278
|
expect(result.data).toBeDefined()
|
|
279
279
|
})
|
|
280
280
|
|
|
281
|
+
// TODO: fix this test
|
|
282
|
+
// https://github.com/opral/inlang-message-sdk/issues/76
|
|
283
|
+
// it doesn't work because failure to open the settings file doesn't throw
|
|
284
|
+
// errors are returned in project.errors()
|
|
281
285
|
it("should resolve from a windows path", async () => {
|
|
282
286
|
const repo = await mockRepo()
|
|
283
287
|
const fs = repo.nodeishFs
|
|
284
|
-
fs.mkdir("C:\\Users\\user\\project.inlang", { recursive: true })
|
|
285
|
-
fs.writeFile("C:\\Users\\user\\project.inlang\\settings.json", JSON.stringify(settings))
|
|
288
|
+
await fs.mkdir("C:\\Users\\user\\project.inlang", { recursive: true })
|
|
289
|
+
await fs.writeFile("C:\\Users\\user\\project.inlang\\settings.json", JSON.stringify(settings))
|
|
286
290
|
|
|
287
291
|
const result = await tryCatch(() =>
|
|
288
292
|
loadProject({
|
package/src/loadProject.ts
CHANGED
|
@@ -4,6 +4,8 @@ import type {
|
|
|
4
4
|
InstalledMessageLintRule,
|
|
5
5
|
InstalledPlugin,
|
|
6
6
|
Subscribable,
|
|
7
|
+
MessageQueryApi,
|
|
8
|
+
MessageLintReportsQueryApi,
|
|
7
9
|
} from "./api.js"
|
|
8
10
|
import { type ImportFunction, resolveModules } from "./resolve-modules/index.js"
|
|
9
11
|
import { TypeCompiler, ValueErrorType } from "@sinclair/typebox/compiler"
|
|
@@ -30,6 +32,10 @@ import { maybeCreateFirstProjectId } from "./migrations/maybeCreateFirstProjectI
|
|
|
30
32
|
import { capture } from "./telemetry/capture.js"
|
|
31
33
|
import { identifyProject } from "./telemetry/groupIdentify.js"
|
|
32
34
|
|
|
35
|
+
import { stubMessagesQuery, stubMessageLintReportsQuery } from "./v2/stubQueryApi.js"
|
|
36
|
+
import type { StoreApi } from "./persistence/storeApi.js"
|
|
37
|
+
import { openStore } from "./persistence/store.js"
|
|
38
|
+
|
|
33
39
|
import _debug from "debug"
|
|
34
40
|
const debug = _debug("sdk:loadProject")
|
|
35
41
|
|
|
@@ -80,9 +86,16 @@ export async function loadProject(args: {
|
|
|
80
86
|
)
|
|
81
87
|
|
|
82
88
|
const [initialized, markInitAsComplete, markInitAsFailed] = createAwaitable()
|
|
89
|
+
const [loadedSettings, markSettingsAsLoaded, markSettingsAsFailed] = createAwaitable()
|
|
83
90
|
// -- settings ------------------------------------------------------------
|
|
84
91
|
|
|
85
92
|
const [settings, _setSettings] = createSignal<ProjectSettings>()
|
|
93
|
+
let v2Persistence = false
|
|
94
|
+
let locales: string[] = []
|
|
95
|
+
|
|
96
|
+
// This effect currently has no signals
|
|
97
|
+
// TODO: replace createEffect with await loadSettings
|
|
98
|
+
// https://github.com/opral/inlang-message-sdk/issues/77
|
|
86
99
|
createEffect(() => {
|
|
87
100
|
// TODO:
|
|
88
101
|
// if (projectId) {
|
|
@@ -92,12 +105,17 @@ export async function loadProject(args: {
|
|
|
92
105
|
// }
|
|
93
106
|
|
|
94
107
|
loadSettings({ settingsFilePath: projectPath + "/settings.json", nodeishFs })
|
|
95
|
-
.then((settings) =>
|
|
108
|
+
.then((settings) => {
|
|
109
|
+
setSettings(settings)
|
|
110
|
+
markSettingsAsLoaded()
|
|
111
|
+
})
|
|
96
112
|
.catch((err) => {
|
|
97
113
|
markInitAsFailed(err)
|
|
114
|
+
markSettingsAsFailed(err)
|
|
98
115
|
})
|
|
99
116
|
})
|
|
100
117
|
// TODO: create FS watcher and update settings on change
|
|
118
|
+
// https://github.com/opral/inlang-message-sdk/issues/35
|
|
101
119
|
|
|
102
120
|
const writeSettingsToDisk = skipFirst((settings: ProjectSettings) =>
|
|
103
121
|
_writeSettingsToDisk({ nodeishFs, settings, projectPath })
|
|
@@ -106,11 +124,8 @@ export async function loadProject(args: {
|
|
|
106
124
|
const setSettings = (settings: ProjectSettings): Result<void, ProjectSettingsInvalidError> => {
|
|
107
125
|
try {
|
|
108
126
|
const validatedSettings = parseSettings(settings)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
pathPattern: projectPath + "/messages.json",
|
|
112
|
-
}
|
|
113
|
-
}
|
|
127
|
+
v2Persistence = !!validatedSettings.experimental?.persistence
|
|
128
|
+
locales = validatedSettings.languageTags
|
|
114
129
|
|
|
115
130
|
batch(() => {
|
|
116
131
|
// reset the resolved modules first - since they are no longer valid at that point
|
|
@@ -147,40 +162,11 @@ export async function loadProject(args: {
|
|
|
147
162
|
.catch((err) => markInitAsFailed(err))
|
|
148
163
|
})
|
|
149
164
|
|
|
150
|
-
// --
|
|
165
|
+
// -- installed items ----------------------------------------------------
|
|
151
166
|
|
|
152
167
|
let settingsValue: ProjectSettings
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const [loadMessagesViaPluginError, setLoadMessagesViaPluginError] = createSignal<
|
|
156
|
-
Error | undefined
|
|
157
|
-
>()
|
|
158
|
-
|
|
159
|
-
const [saveMessagesViaPluginError, setSaveMessagesViaPluginError] = createSignal<
|
|
160
|
-
Error | undefined
|
|
161
|
-
>()
|
|
162
|
-
|
|
163
|
-
const messagesQuery = createMessagesQuery({
|
|
164
|
-
projectPath,
|
|
165
|
-
nodeishFs,
|
|
166
|
-
settings,
|
|
167
|
-
resolvedModules,
|
|
168
|
-
onInitialMessageLoadResult: (e) => {
|
|
169
|
-
if (e) {
|
|
170
|
-
markInitAsFailed(e)
|
|
171
|
-
} else {
|
|
172
|
-
markInitAsComplete()
|
|
173
|
-
}
|
|
174
|
-
},
|
|
175
|
-
onLoadMessageResult: (e) => {
|
|
176
|
-
setLoadMessagesViaPluginError(e)
|
|
177
|
-
},
|
|
178
|
-
onSaveMessageResult: (e) => {
|
|
179
|
-
setSaveMessagesViaPluginError(e)
|
|
180
|
-
},
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
// -- installed items ----------------------------------------------------
|
|
168
|
+
// workaround to not run effects twice (e.g. settings change + modules change) (I'm sure there exists a solid way of doing this, but I haven't found it yet)
|
|
169
|
+
createEffect(() => (settingsValue = settings()!))
|
|
184
170
|
|
|
185
171
|
const installedMessageLintRules = () => {
|
|
186
172
|
if (!resolvedModules()) return []
|
|
@@ -213,17 +199,69 @@ export async function loadProject(args: {
|
|
|
213
199
|
})) satisfies Array<InstalledPlugin>
|
|
214
200
|
}
|
|
215
201
|
|
|
202
|
+
// -- messages ----------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
const [loadMessagesViaPluginError, setLoadMessagesViaPluginError] = createSignal<
|
|
205
|
+
Error | undefined
|
|
206
|
+
>()
|
|
207
|
+
|
|
208
|
+
const [saveMessagesViaPluginError, setSaveMessagesViaPluginError] = createSignal<
|
|
209
|
+
Error | undefined
|
|
210
|
+
>()
|
|
211
|
+
|
|
212
|
+
let messagesQuery: MessageQueryApi
|
|
213
|
+
let lintReportsQuery: MessageLintReportsQueryApi
|
|
214
|
+
let store: StoreApi | undefined
|
|
215
|
+
|
|
216
|
+
// wait for seetings to load v2Persistence flag
|
|
217
|
+
// .catch avoids throwing here if the awaitable is rejected
|
|
218
|
+
// error is recorded via markInitAsFailed so no need to capture it again
|
|
219
|
+
await loadedSettings.catch(() => {})
|
|
220
|
+
|
|
221
|
+
if (v2Persistence) {
|
|
222
|
+
messagesQuery = stubMessagesQuery
|
|
223
|
+
lintReportsQuery = stubMessageLintReportsQuery
|
|
224
|
+
try {
|
|
225
|
+
store = await openStore({ projectPath, nodeishFs, locales })
|
|
226
|
+
markInitAsComplete()
|
|
227
|
+
} catch (e) {
|
|
228
|
+
markInitAsFailed(e)
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
messagesQuery = createMessagesQuery({
|
|
232
|
+
projectPath,
|
|
233
|
+
nodeishFs,
|
|
234
|
+
settings,
|
|
235
|
+
resolvedModules,
|
|
236
|
+
onInitialMessageLoadResult: (e) => {
|
|
237
|
+
if (e) {
|
|
238
|
+
markInitAsFailed(e)
|
|
239
|
+
} else {
|
|
240
|
+
markInitAsComplete()
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
onLoadMessageResult: (e) => {
|
|
244
|
+
setLoadMessagesViaPluginError(e)
|
|
245
|
+
},
|
|
246
|
+
onSaveMessageResult: (e) => {
|
|
247
|
+
setSaveMessagesViaPluginError(e)
|
|
248
|
+
},
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
lintReportsQuery = createMessageLintReportsQuery(
|
|
252
|
+
messagesQuery,
|
|
253
|
+
settings as () => ProjectSettings,
|
|
254
|
+
installedMessageLintRules,
|
|
255
|
+
resolvedModules
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
store = undefined
|
|
259
|
+
}
|
|
260
|
+
|
|
216
261
|
// -- app ---------------------------------------------------------------
|
|
217
262
|
|
|
218
263
|
const initializeError: Error | undefined = await initialized.catch((error) => error)
|
|
219
264
|
|
|
220
|
-
const lintReportsQuery = createMessageLintReportsQuery(
|
|
221
|
-
messagesQuery,
|
|
222
|
-
settings as () => ProjectSettings,
|
|
223
|
-
installedMessageLintRules,
|
|
224
|
-
resolvedModules
|
|
225
|
-
)
|
|
226
|
-
|
|
227
265
|
/**
|
|
228
266
|
* Utility to escape reactive tracking and avoid multiple calls to
|
|
229
267
|
* the capture event.
|
|
@@ -250,6 +288,8 @@ export async function loadProject(args: {
|
|
|
250
288
|
settings: settings(),
|
|
251
289
|
installedPluginIds: installedPlugins().map((p) => p.id),
|
|
252
290
|
installedMessageLintRuleIds: installedMessageLintRules().map((r) => r.id),
|
|
291
|
+
// TODO: fix for v2Persistence
|
|
292
|
+
// https://github.com/opral/inlang-message-sdk/issues/78
|
|
253
293
|
numberOfMessages: messagesQuery.includedMessageIds().length,
|
|
254
294
|
},
|
|
255
295
|
})
|
|
@@ -276,6 +316,7 @@ export async function loadProject(args: {
|
|
|
276
316
|
messages: messagesQuery,
|
|
277
317
|
messageLintReports: lintReportsQuery,
|
|
278
318
|
},
|
|
319
|
+
store,
|
|
279
320
|
} satisfies InlangProject
|
|
280
321
|
})
|
|
281
322
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest"
|
|
2
|
+
import { sleep } from "../test-utilities/sleep.js"
|
|
3
|
+
import { batchedIO } from "./batchedIO.js"
|
|
4
|
+
|
|
5
|
+
let locked = false
|
|
6
|
+
|
|
7
|
+
const instrumentAquireLockStart = vi.fn()
|
|
8
|
+
|
|
9
|
+
async function mockAquireLock() {
|
|
10
|
+
instrumentAquireLockStart()
|
|
11
|
+
let pollCount = 0
|
|
12
|
+
while (locked && pollCount++ < 100) {
|
|
13
|
+
await sleep(10)
|
|
14
|
+
}
|
|
15
|
+
if (locked) {
|
|
16
|
+
throw new Error("Timeout acquiring lock")
|
|
17
|
+
}
|
|
18
|
+
await sleep(10)
|
|
19
|
+
locked = true
|
|
20
|
+
return 69
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
24
|
+
async function mockReleaseLock(_: number) {
|
|
25
|
+
sleep(10)
|
|
26
|
+
locked = false
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const instrumentSaveStart = vi.fn()
|
|
30
|
+
const instrumentSaveEnd = vi.fn()
|
|
31
|
+
|
|
32
|
+
async function mockSave() {
|
|
33
|
+
instrumentSaveStart()
|
|
34
|
+
await sleep(50)
|
|
35
|
+
instrumentSaveEnd()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("batchedIO", () => {
|
|
39
|
+
it("queues 2 requests while waiting for lock and pushes 2 more to the next batch", async () => {
|
|
40
|
+
const save = batchedIO(mockAquireLock, mockReleaseLock, mockSave)
|
|
41
|
+
const p1 = save("1")
|
|
42
|
+
const p2 = save("2")
|
|
43
|
+
await sleep(5)
|
|
44
|
+
expect(instrumentAquireLockStart).toHaveBeenCalledTimes(1)
|
|
45
|
+
expect(instrumentSaveStart).not.toHaveBeenCalled()
|
|
46
|
+
await sleep(10)
|
|
47
|
+
expect(instrumentSaveStart).toHaveBeenCalled()
|
|
48
|
+
expect(instrumentSaveEnd).not.toHaveBeenCalled()
|
|
49
|
+
const p3 = save("3")
|
|
50
|
+
const p4 = save("4")
|
|
51
|
+
expect(instrumentAquireLockStart).toHaveBeenCalledTimes(2)
|
|
52
|
+
expect(locked).toBe(true)
|
|
53
|
+
await sleep(50)
|
|
54
|
+
expect(instrumentSaveEnd).toHaveBeenCalled()
|
|
55
|
+
expect(await p1).toBe("1")
|
|
56
|
+
expect(await p2).toBe("2")
|
|
57
|
+
expect(instrumentSaveStart).toHaveBeenCalledTimes(1)
|
|
58
|
+
expect(await p3).toBe("3")
|
|
59
|
+
expect(await p4).toBe("4")
|
|
60
|
+
expect(instrumentAquireLockStart).toHaveBeenCalledTimes(2)
|
|
61
|
+
expect(instrumentSaveStart).toHaveBeenCalledTimes(2)
|
|
62
|
+
})
|
|
63
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import _debug from "debug"
|
|
2
|
+
const debug = _debug("sdk:batchedIO")
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* State machine to convert async save() into batched async save()
|
|
6
|
+
* states = idle -> acquiring -> saving -> idle
|
|
7
|
+
* idle = nothing queued, ready to acquire lock.
|
|
8
|
+
* aquiring = waiting to acquire a lock: requests go into the queue.
|
|
9
|
+
* saving = lock is acquired, save has begun: new requests go into the next batch.
|
|
10
|
+
* The next batch should not acquire the lock while current save is in progress.
|
|
11
|
+
* Queued requests are only resolved when the save completes.
|
|
12
|
+
*/
|
|
13
|
+
export function batchedIO(
|
|
14
|
+
acquireLock: () => Promise<number>,
|
|
15
|
+
releaseLock: (lock: number) => Promise<void>,
|
|
16
|
+
save: () => Promise<void>
|
|
17
|
+
): (id: string) => Promise<string> {
|
|
18
|
+
// 3-state machine
|
|
19
|
+
let state: "idle" | "acquiring" | "saving" = "idle"
|
|
20
|
+
|
|
21
|
+
// Hold requests while acquiring, resolve after saving
|
|
22
|
+
// TODO: rejectQueued if save throws (maybe?)
|
|
23
|
+
// https://github.com/opral/inlang-message-sdk/issues/79
|
|
24
|
+
type Queued = {
|
|
25
|
+
id: string
|
|
26
|
+
resolve: (value: string) => void
|
|
27
|
+
reject: (reason: any) => void
|
|
28
|
+
}
|
|
29
|
+
const queue: Queued[] = []
|
|
30
|
+
|
|
31
|
+
// initialize nextBatch lazily, reset after saving
|
|
32
|
+
let nextBatch: ((id: string) => Promise<string>) | undefined = undefined
|
|
33
|
+
|
|
34
|
+
// batched save function
|
|
35
|
+
return async (id: string) => {
|
|
36
|
+
if (state === "idle") {
|
|
37
|
+
state = "acquiring"
|
|
38
|
+
const lock = await acquireLock()
|
|
39
|
+
state = "saving"
|
|
40
|
+
await save()
|
|
41
|
+
await releaseLock(lock)
|
|
42
|
+
resolveQueued()
|
|
43
|
+
nextBatch = undefined
|
|
44
|
+
state = "idle"
|
|
45
|
+
return id
|
|
46
|
+
} else if (state === "acquiring") {
|
|
47
|
+
return new Promise<string>((resolve, reject) => {
|
|
48
|
+
queue.push({ id, resolve, reject })
|
|
49
|
+
})
|
|
50
|
+
} else {
|
|
51
|
+
// state === "saving"
|
|
52
|
+
nextBatch = nextBatch ?? batchedIO(acquireLock, releaseLock, save)
|
|
53
|
+
return await nextBatch(id)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function resolveQueued() {
|
|
58
|
+
debug("batched", queue.length + 1)
|
|
59
|
+
for (const { id, resolve } of queue) {
|
|
60
|
+
resolve(id)
|
|
61
|
+
}
|
|
62
|
+
queue.length = 0
|
|
63
|
+
}
|
|
64
|
+
}
|