@fedify/vocab 2.0.0-dev.241 → 2.0.0-dev.323

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.
@@ -2,9 +2,10 @@
2
2
  import { Temporal } from "@js-temporal/polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
 
5
- import { Activity, Announce, Collection, Create, CryptographicKey, Follow, Hashtag, LanguageString, Link, Note, Object as Object$1, OrderedCollectionPage, Person, Place, Question, Source, decodeMultibase, mockDocumentLoader, test, vocab_exports } from "./vocab-BmxSLhXr.js";
5
+ import { Activity, Announce, Collection, Create, CryptographicKey, Follow, Hashtag, Link, Note, Object as Object$1, OrderedCollectionPage, Person, Place, Question, Source, mockDocumentLoader, test, vocab_exports } from "./vocab-B4nUNXTL.js";
6
6
  import { assertInstanceOf } from "./utils-Dm0Onkcz.js";
7
7
  import { deepStrictEqual, notDeepStrictEqual, ok, rejects, throws } from "node:assert/strict";
8
+ import { LanguageString, decodeMultibase } from "@fedify/vocab-runtime";
8
9
  import { pascalCase } from "es-toolkit";
9
10
  import { areAllScalarTypes, loadSchemaFiles } from "@fedify/vocab-tools";
10
11
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/vocab",
3
- "version": "2.0.0-dev.241+58c8126c",
3
+ "version": "2.0.0-dev.323+1d796545",
4
4
  "homepage": "https://fedify.dev/",
5
5
  "repository": {
6
6
  "type": "git",
@@ -47,8 +47,9 @@
47
47
  "jsonld": "^9.0.0",
48
48
  "multicodec": "^3.2.1",
49
49
  "pkijs": "^3.3.3",
50
- "@fedify/vocab-tools": "2.0.0-dev.241+58c8126c",
51
- "@fedify/webfinger": "2.0.0-dev.241+58c8126c"
50
+ "@fedify/webfinger": "2.0.0-dev.323+1d796545",
51
+ "@fedify/vocab-runtime": "2.0.0-dev.323+1d796545",
52
+ "@fedify/vocab-tools": "2.0.0-dev.323+1d796545"
52
53
  },
53
54
  "devDependencies": {
54
55
  "@types/node": "^22.17.0",
@@ -56,8 +57,7 @@
56
57
  "fetch-mock": "^12.5.4",
57
58
  "tsdown": "^0.12.9",
58
59
  "typescript": "^5.9.3",
59
- "@fedify/fixture": "2.0.0",
60
- "@fedify/vocab-runtime": "2.0.0-dev.241+58c8126c"
60
+ "@fedify/fixture": "2.0.0"
61
61
  },
62
62
  "keywords": [
63
63
  "Fedify",
@@ -74,7 +74,9 @@
74
74
  "build:self": "deno task compile && tsdown",
75
75
  "build": "pnpm --filter @fedify/vocab... run build:self",
76
76
  "prepublish": "pnpm build",
77
- "test": "pnpm build && cd dist/ && node --test",
78
- "test:bun": "pnpm build && cd dist/ && bun test --timeout 60000"
77
+ "pretest": "pnpm build",
78
+ "test": "cd dist/ && node --test",
79
+ "pretest:bun": "pnpm build",
80
+ "test:bun": "cd dist/ && bun test --timeout 60000"
79
81
  }
80
82
  }
@@ -1,18 +1,154 @@
1
+ import $ from "@david/dax";
2
+ import type { Path } from "@david/dax";
1
3
  import { generateVocab } from "@fedify/vocab-tools";
2
- import { rename } from "node:fs/promises";
3
- import { dirname, join } from "node:path";
4
4
 
5
- async function codegen() {
6
- const scriptsDir = import.meta.dirname;
7
- if (!scriptsDir) {
8
- throw new Error("Could not determine schema directory");
5
+ const LOCK_STALE_MS = 5 * 60 * 1000; // 5 minutes
6
+ const LOCK_RETRY_MS = 100;
7
+ const LOCK_TIMEOUT_MS = 60 * 1000; // 1 minute
8
+
9
+ /**
10
+ * Get the latest mtime from all YAML files in the schema directory.
11
+ */
12
+ async function getLatestSourceMtime(schemaDir: Path): Promise<number> {
13
+ let latestMtime = 0;
14
+ for await (const entry of schemaDir.readDir()) {
15
+ if (!entry.isFile) continue;
16
+ if (!entry.name.match(/\.ya?ml$/i)) continue;
17
+ if (entry.name === "schema.yaml") continue;
18
+ const fileStat = await schemaDir.join(entry.name).stat();
19
+ if (fileStat?.mtime && fileStat.mtime.getTime() > latestMtime) {
20
+ latestMtime = fileStat.mtime.getTime();
21
+ }
22
+ }
23
+ return latestMtime;
24
+ }
25
+
26
+ /**
27
+ * Check if the generated file is up to date compared to source files.
28
+ */
29
+ async function isUpToDate(
30
+ schemaDir: Path,
31
+ generatedPath: Path,
32
+ ): Promise<boolean> {
33
+ try {
34
+ const [sourceMtime, generatedStat] = await Promise.all([
35
+ getLatestSourceMtime(schemaDir),
36
+ generatedPath.stat(),
37
+ ]);
38
+ if (!generatedStat?.mtime) return false;
39
+ return generatedStat.mtime.getTime() >= sourceMtime;
40
+ } catch {
41
+ // If generated file doesn't exist, it's not up to date
42
+ return false;
43
+ }
44
+ }
45
+
46
+ interface Lock {
47
+ release(): Promise<void>;
48
+ }
49
+
50
+ /**
51
+ * Acquire a directory-based lock. mkdir is atomic on POSIX systems.
52
+ */
53
+ async function acquireLock(lockPath: Path): Promise<Lock> {
54
+ const startTime = Date.now();
55
+
56
+ while (true) {
57
+ try {
58
+ // Use Deno.mkdir directly because dax's mkdir() is recursive by default
59
+ await Deno.mkdir(lockPath.toString());
60
+ // Write PID and timestamp for stale lock detection
61
+ const infoPath = lockPath.join("info");
62
+ await infoPath.writeJsonPretty({ pid: Deno.pid, timestamp: Date.now() });
63
+ return {
64
+ async release() {
65
+ try {
66
+ await lockPath.remove({ recursive: true });
67
+ } catch {
68
+ // Ignore errors during cleanup
69
+ }
70
+ },
71
+ };
72
+ } catch (e) {
73
+ if (!(e instanceof Deno.errors.AlreadyExists)) {
74
+ throw e;
75
+ }
76
+
77
+ // Check if lock is stale
78
+ try {
79
+ const infoPath = lockPath.join("info");
80
+ const infoStat = await infoPath.stat();
81
+ if (
82
+ infoStat?.mtime &&
83
+ Date.now() - infoStat.mtime.getTime() > LOCK_STALE_MS
84
+ ) {
85
+ console.warn("Removing stale lock:", lockPath.toString());
86
+ await lockPath.remove({ recursive: true });
87
+ continue;
88
+ }
89
+ } catch {
90
+ // If we can't read the info file, try to remove the lock
91
+ try {
92
+ await lockPath.remove({ recursive: true });
93
+ continue;
94
+ } catch {
95
+ // Ignore
96
+ }
97
+ }
98
+
99
+ // Check timeout
100
+ if (Date.now() - startTime > LOCK_TIMEOUT_MS) {
101
+ throw new Error(`Timeout waiting for lock: ${lockPath}`);
102
+ }
103
+
104
+ // Wait and retry
105
+ await $.sleep(LOCK_RETRY_MS);
106
+ }
9
107
  }
10
- const schemaDir = join(dirname(scriptsDir), "src");
11
- const generatedPath = join(schemaDir, `vocab-${crypto.randomUUID()}.ts`);
12
- const realPath = join(schemaDir, "vocab.ts");
108
+ }
109
+
110
+ async function codegen() {
111
+ const scriptsDir = $.path(import.meta.dirname!);
112
+ const packageDir = scriptsDir.parent()!;
113
+ const schemaDir = packageDir.join("src");
114
+ const realPath = schemaDir.join("vocab.ts");
115
+ const lockPath = packageDir.join(".vocab-codegen.lock");
116
+
117
+ // Acquire lock to prevent concurrent codegen
118
+ const lock = await acquireLock(lockPath);
119
+ try {
120
+ // Check if regeneration is needed (after acquiring lock)
121
+ if (await isUpToDate(schemaDir, realPath)) {
122
+ $.log("vocab.ts is up to date, skipping codegen");
123
+ return;
124
+ }
13
125
 
14
- await generateVocab(schemaDir, generatedPath);
15
- await rename(generatedPath, realPath);
126
+ $.logStep("Generating", "vocab.ts...");
127
+
128
+ // Generate to a temporary file first
129
+ const generatedPath = schemaDir.join(`vocab-${crypto.randomUUID()}.ts`);
130
+ try {
131
+ await generateVocab(schemaDir.toString(), generatedPath.toString());
132
+ await generatedPath.rename(realPath);
133
+ } catch (e) {
134
+ // Clean up temp file on error
135
+ await generatedPath.remove().catch(() => {});
136
+ throw e;
137
+ }
138
+
139
+ $.logStep("Formatting", "vocab.ts...");
140
+ await $`deno fmt ${realPath}`;
141
+
142
+ $.logStep("Caching", "vocab.ts...");
143
+ await $`deno cache ${realPath}`;
144
+
145
+ $.logStep("Type checking", "vocab.ts...");
146
+ await $`deno check ${realPath}`;
147
+
148
+ $.logStep("Codegen", "completed successfully");
149
+ } finally {
150
+ await lock.release();
151
+ }
16
152
  }
17
153
 
18
154
  if (import.meta.main) {
@@ -267,6 +267,21 @@ test("traverseCollection()", {
267
267
  new Note({ content: "This is a third simple note" }),
268
268
  ],
269
269
  );
270
+ // Inline-paged collection (CollectionPage embedded without id, with next)
271
+ const inlinePagedCollection = await lookupObject(
272
+ "https://example.com/inline-paged-collection",
273
+ options,
274
+ );
275
+ assertInstanceOf(inlinePagedCollection, Collection);
276
+ deepStrictEqual(
277
+ await Array.fromAsync(
278
+ traverseCollection(inlinePagedCollection, options),
279
+ ),
280
+ [
281
+ new Note({ content: "Inline first note" }),
282
+ new Note({ content: "Inline second note" }),
283
+ ],
284
+ );
270
285
  });
271
286
 
272
287
  test("FEP-fe34: lookupObject() cross-origin security", {
package/src/lookup.ts CHANGED
@@ -299,14 +299,14 @@ export async function* traverseCollection(
299
299
  collection: Collection,
300
300
  options: TraverseCollectionOptions = {},
301
301
  ): AsyncIterable<Object | Link> {
302
- if (collection.firstId == null) {
302
+ const interval = Temporal.Duration.from(options.interval ?? { seconds: 0 })
303
+ .total("millisecond");
304
+ let page = await collection.getFirst(options);
305
+ if (page == null) {
303
306
  for await (const item of collection.getItems(options)) {
304
307
  yield item;
305
308
  }
306
309
  } else {
307
- const interval = Temporal.Duration.from(options.interval ?? { seconds: 0 })
308
- .total("millisecond");
309
- let page = await collection.getFirst(options);
310
310
  while (page != null) {
311
311
  for await (const item of page.getItems(options)) {
312
312
  yield item;
package/src/mod.ts CHANGED
@@ -55,3 +55,9 @@ export * from "./handle.ts";
55
55
  export * from "./lookup.ts";
56
56
  export * from "./type.ts";
57
57
  export * from "./vocab.ts";
58
+ export { LanguageString } from "@fedify/vocab-runtime";
59
+ export type {
60
+ DocumentLoader,
61
+ GetUserAgentOptions,
62
+ RemoteDocument,
63
+ } from "@fedify/vocab-runtime";