@noya-app/noya-multiplayer-react 0.1.61 → 0.1.63

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.
@@ -0,0 +1,464 @@
1
+ /* eslint-disable @shopify/prefer-early-return */
2
+
3
+ // TinyZip.ts — ultra-lightweight ZIP writer (no deps)
4
+ // - UTF-8 file names, Store only (no compression)
5
+ // - Optional top-level root folder so unzip creates a single directory
6
+ // - No Zip64 (files and total size must be < 4 GiB)
7
+
8
+ import { crc32 } from "./crc32";
9
+ import { defineSchema, defineSchemas, readStruct, writeStruct } from "./struct";
10
+
11
+ export interface AddFileOptions {
12
+ date?: Date;
13
+ }
14
+
15
+ export interface TinyZipOptions {
16
+ /** Place all entries under this top-level folder; creates the dir entry automatically. */
17
+ root?: string;
18
+ }
19
+
20
+ interface CentralEntry {
21
+ nameBytes: Uint8Array;
22
+ crc: number;
23
+ compSize: number;
24
+ uncompSize: number;
25
+ method: number;
26
+ time: number;
27
+ dosDate: number;
28
+ localHeaderOffset: number;
29
+ externalAttrs: number; // DOS attrs low byte (0x10 = directory)
30
+ }
31
+
32
+ // -------------------------
33
+ // Top-level helpers (no state)
34
+ // -------------------------
35
+ const te = new TextEncoder();
36
+
37
+ function utf8(s: string): Uint8Array {
38
+ return te.encode(s);
39
+ }
40
+
41
+ function dosDateTime(d: Date = new Date()): {
42
+ time: number;
43
+ date: number;
44
+ } {
45
+ const time =
46
+ (d.getHours() << 11) |
47
+ (d.getMinutes() << 5) |
48
+ Math.floor(d.getSeconds() / 2);
49
+ const date =
50
+ ((d.getFullYear() - 1980) << 9) | ((d.getMonth() + 1) << 5) | d.getDate();
51
+ return { time, date };
52
+ }
53
+
54
+ // ZIP constants
55
+ const SIG = {
56
+ LocalFile: 0x04034b50,
57
+ CentralFile: 0x02014b50,
58
+ EOCD: 0x06054b50,
59
+ } as const;
60
+
61
+ const SCHEMA = defineSchemas({
62
+ LocalFile: defineSchema([
63
+ { name: "signature", type: "u32" },
64
+ { name: "versionNeeded", type: "u16" },
65
+ { name: "flags", type: "u16" },
66
+ { name: "method", type: "u16" },
67
+ { name: "time", type: "u16" },
68
+ { name: "date", type: "u16" },
69
+ { name: "crc32", type: "u32" },
70
+ { name: "compSize", type: "u32" },
71
+ { name: "uncompSize", type: "u32" },
72
+ { name: "nameLen", type: "u16" },
73
+ { name: "extraLen", type: "u16" },
74
+ { name: "name", type: "bytes", length: (c) => c.nameLen },
75
+ { name: "extra", type: "bytes", length: (c) => c.extraLen },
76
+ ]),
77
+ CentralFile: defineSchema([
78
+ { name: "signature", type: "u32" },
79
+ { name: "versionMadeBy", type: "u16" },
80
+ { name: "versionNeeded", type: "u16" },
81
+ { name: "flags", type: "u16" },
82
+ { name: "method", type: "u16" },
83
+ { name: "time", type: "u16" },
84
+ { name: "date", type: "u16" },
85
+ { name: "crc32", type: "u32" },
86
+ { name: "compSize", type: "u32" },
87
+ { name: "uncompSize", type: "u32" },
88
+ { name: "nameLen", type: "u16" },
89
+ { name: "extraLen", type: "u16" },
90
+ { name: "commentLen", type: "u16" },
91
+ { name: "diskStart", type: "u16" },
92
+ { name: "intAttrs", type: "u16" },
93
+ { name: "extAttrs", type: "u32" },
94
+ { name: "localHeaderOffset", type: "u32" },
95
+ { name: "name", type: "bytes", length: (c) => c.nameLen },
96
+ { name: "extra", type: "bytes", length: (c) => c.extraLen },
97
+ { name: "comment", type: "bytes", length: (c) => c.commentLen },
98
+ ]),
99
+ EOCD: defineSchema([
100
+ { name: "signature", type: "u32" },
101
+ { name: "diskNum", type: "u16" },
102
+ { name: "cdStartDisk", type: "u16" },
103
+ { name: "cdRecordsOnDisk", type: "u16" },
104
+ { name: "cdRecordsTotal", type: "u16" },
105
+ { name: "cdSize", type: "u32" },
106
+ { name: "cdOffset", type: "u32" },
107
+ { name: "commentLen", type: "u16" },
108
+ { name: "comment", type: "bytes", length: (c) => c.commentLen },
109
+ ]),
110
+ });
111
+
112
+ function ensureSig(v: number, expect: number, where: string) {
113
+ if (v !== expect)
114
+ throw new Error(`Invalid signature for ${where}: 0x${v.toString(16)}`);
115
+ }
116
+
117
+ // -------------------------
118
+ // TinyZip (writer)
119
+ // -------------------------
120
+ export class TinyZip {
121
+ private _chunks: Uint8Array[] = [];
122
+ private _offset = 0;
123
+ private _files: CentralEntry[] = [];
124
+
125
+ private _rootPrefix = ""; // e.g. "my-archive/" or ""
126
+ private _rootDirAdded = false;
127
+
128
+ constructor(opts: TinyZipOptions = {}) {
129
+ if (opts.root) {
130
+ const r = opts.root.replace(/^\/+|\/+$/g, "");
131
+ if (r) this._rootPrefix = r + "/";
132
+ }
133
+ }
134
+
135
+ private _push(...arrs: Uint8Array[]) {
136
+ for (const a of arrs) {
137
+ this._chunks.push(a);
138
+ this._offset += a.length;
139
+ }
140
+ }
141
+ private _withRoot(name: string): string {
142
+ return this._rootPrefix && !name.startsWith(this._rootPrefix)
143
+ ? this._rootPrefix + name.replace(/^\/+/, "")
144
+ : name;
145
+ }
146
+
147
+ private _ensureRootDir(date: Date = new Date()) {
148
+ if (!this._rootPrefix || this._rootDirAdded) return;
149
+ this._rootDirAdded = true;
150
+
151
+ const dirName = this._rootPrefix; // already ends with '/'
152
+ const nameBytes = utf8(dirName);
153
+ const { time, date: dosDate } = dosDateTime(date);
154
+
155
+ const localHeader = writeStruct(SCHEMA.LocalFile, {
156
+ signature: SIG.LocalFile,
157
+ versionNeeded: 20,
158
+ flags: 0x0800,
159
+ method: 0,
160
+ time,
161
+ date: dosDate,
162
+ crc32: 0,
163
+ compSize: 0,
164
+ uncompSize: 0,
165
+ nameLen: nameBytes.length,
166
+ extraLen: 0,
167
+ name: nameBytes,
168
+ extra: new Uint8Array(0),
169
+ });
170
+
171
+ const localHeaderOffset = this._offset;
172
+ this._push(localHeader);
173
+ this._files.push({
174
+ nameBytes,
175
+ crc: 0,
176
+ compSize: 0,
177
+ uncompSize: 0,
178
+ method: 0,
179
+ time,
180
+ dosDate,
181
+ localHeaderOffset,
182
+ externalAttrs: 0x10,
183
+ });
184
+ }
185
+
186
+ /** Add a file to the zip. "name" uses forward slashes for paths (e.g., "dir/file.txt"). */
187
+ addFile(
188
+ name: string,
189
+ data: string | Uint8Array | ArrayBuffer,
190
+ opts: AddFileOptions = {}
191
+ ): void {
192
+ const { date = new Date() } = opts;
193
+ this._ensureRootDir(date);
194
+
195
+ const u8 = toU8(data);
196
+
197
+ const fullName = this._withRoot(name);
198
+ const nameBytes = utf8(fullName);
199
+ const { time, date: dosDate } = dosDateTime(date);
200
+ const crc = crc32(u8);
201
+
202
+ const compSize = u8.length >>> 0;
203
+ const uncompSize = u8.length >>> 0;
204
+ const localHeaderOffset = this._offset;
205
+
206
+ const localHeader = writeStruct(SCHEMA.LocalFile, {
207
+ signature: SIG.LocalFile,
208
+ versionNeeded: 20,
209
+ flags: 0x0800,
210
+ method: 0,
211
+ time,
212
+ date: dosDate,
213
+ crc32: crc,
214
+ compSize,
215
+ uncompSize,
216
+ nameLen: nameBytes.length,
217
+ extraLen: 0,
218
+ name: nameBytes,
219
+ extra: new Uint8Array(0),
220
+ });
221
+
222
+ this._push(localHeader, u8);
223
+
224
+ this._files.push({
225
+ nameBytes,
226
+ crc,
227
+ compSize,
228
+ uncompSize,
229
+ method: 0,
230
+ time,
231
+ dosDate,
232
+ localHeaderOffset,
233
+ externalAttrs: 0,
234
+ });
235
+ }
236
+
237
+ // Create an explicit directory entry (name should end with "/").
238
+ addDirectory(name: string, opts: { date?: Date } = {}): void {
239
+ const { date = new Date() } = opts;
240
+ this._ensureRootDir(date);
241
+
242
+ const dirNameRaw = name.endsWith("/") ? name : name + "/";
243
+ const fullName = this._withRoot(dirNameRaw);
244
+ const nameBytes = utf8(fullName);
245
+ const { time, date: dosDate } = dosDateTime(date);
246
+
247
+ const localHeader = writeStruct(SCHEMA.LocalFile, {
248
+ signature: SIG.LocalFile,
249
+ versionNeeded: 20,
250
+ flags: 0x0800,
251
+ method: 0,
252
+ time,
253
+ date: dosDate,
254
+ crc32: 0,
255
+ compSize: 0,
256
+ uncompSize: 0,
257
+ nameLen: nameBytes.length,
258
+ extraLen: 0,
259
+ name: nameBytes,
260
+ extra: new Uint8Array(0),
261
+ });
262
+
263
+ const localHeaderOffset = this._offset;
264
+ this._push(localHeader);
265
+
266
+ this._files.push({
267
+ nameBytes,
268
+ crc: 0,
269
+ compSize: 0,
270
+ uncompSize: 0,
271
+ method: 0,
272
+ time,
273
+ dosDate,
274
+ localHeaderOffset,
275
+ externalAttrs: 0x10,
276
+ });
277
+ }
278
+
279
+ finish(): Uint8Array {
280
+ const cdStart = this._offset;
281
+
282
+ for (const f of this._files) {
283
+ const central = writeStruct(SCHEMA.CentralFile, {
284
+ signature: SIG.CentralFile,
285
+ versionMadeBy: 20,
286
+ versionNeeded: 20,
287
+ flags: 0x0800,
288
+ method: f.method,
289
+ time: f.time,
290
+ date: f.dosDate,
291
+ crc32: f.crc,
292
+ compSize: f.compSize,
293
+ uncompSize: f.uncompSize,
294
+ nameLen: f.nameBytes.length,
295
+ extraLen: 0,
296
+ commentLen: 0,
297
+ diskStart: 0,
298
+ intAttrs: 0,
299
+ extAttrs: f.externalAttrs,
300
+ localHeaderOffset: f.localHeaderOffset,
301
+ name: f.nameBytes,
302
+ extra: new Uint8Array(0),
303
+ comment: new Uint8Array(0),
304
+ });
305
+ this._push(central);
306
+ }
307
+
308
+ const cdSize = this._offset - cdStart;
309
+
310
+ const eocd = writeStruct(SCHEMA.EOCD, {
311
+ signature: SIG.EOCD,
312
+ diskNum: 0,
313
+ cdStartDisk: 0,
314
+ cdRecordsOnDisk: this._files.length,
315
+ cdRecordsTotal: this._files.length,
316
+ cdSize,
317
+ cdOffset: cdStart,
318
+ commentLen: 0,
319
+ comment: new Uint8Array(0),
320
+ });
321
+
322
+ this._push(eocd);
323
+
324
+ const merged = new Uint8Array(this._offset);
325
+ let offset = 0;
326
+ for (const chunk of this._chunks) {
327
+ merged.set(chunk, offset);
328
+ offset += chunk.length;
329
+ }
330
+ return merged;
331
+ }
332
+
333
+ static create(
334
+ files: Record<string, string | Uint8Array | ArrayBuffer>,
335
+ opts: TinyZipOptions = {}
336
+ ): Uint8Array {
337
+ const zip = new TinyZip(opts);
338
+ for (const [name, data] of Object.entries(files)) {
339
+ if (name.endsWith("/")) zip.addDirectory(name);
340
+ else zip.addFile(name, data);
341
+ }
342
+ return zip.finish();
343
+ }
344
+
345
+ static parse(input: ArrayBuffer | Uint8Array): ParsedEntry[] {
346
+ const u8 = toU8(input);
347
+ const view = new DataView(u8.buffer, u8.byteOffset, u8.byteLength);
348
+
349
+ // EOCD
350
+ const eocdOff = findEOCDOffset(u8);
351
+ const { value: eocd } = readStruct(view, eocdOff, SCHEMA.EOCD);
352
+ ensureSig(eocd.signature, SIG.EOCD, "EOCD");
353
+ if (eocd.commentLen !== 0) throw new Error("Unsupported: EOCD comment");
354
+ if (eocd.diskNum !== 0 || eocd.cdStartDisk !== 0)
355
+ throw new Error("Unsupported: multi-disk");
356
+
357
+ // Central directory walk
358
+ const entries: {
359
+ name: string;
360
+ isDirectory: boolean;
361
+ crc32: number;
362
+ size: number;
363
+ compSize: number;
364
+ time: number;
365
+ dosDate: number;
366
+ localHeaderOffset: number;
367
+ }[] = [];
368
+ let cdPtr = eocd.cdOffset;
369
+ for (let i = 0; i < eocd.cdRecordsTotal; i++) {
370
+ const { value: cdf, offset: next } = readStruct(
371
+ view,
372
+ cdPtr,
373
+ SCHEMA.CentralFile
374
+ );
375
+ ensureSig(cdf.signature, SIG.CentralFile, "Central Directory");
376
+ const name = new TextDecoder().decode(cdf.name);
377
+ const isDir = (cdf.extAttrs & 0x10) !== 0 || name.endsWith("/");
378
+ if (cdf.extraLen !== 0 || cdf.commentLen !== 0)
379
+ throw new Error("Unsupported: extra/comment in central dir");
380
+ entries.push({
381
+ name,
382
+ isDirectory: isDir,
383
+ crc32: cdf.crc32 >>> 0,
384
+ size: cdf.uncompSize >>> 0,
385
+ compSize: cdf.compSize >>> 0,
386
+ time: cdf.time >>> 0,
387
+ dosDate: cdf.date >>> 0,
388
+ localHeaderOffset: cdf.localHeaderOffset >>> 0,
389
+ });
390
+ cdPtr = next;
391
+ }
392
+
393
+ // Resolve data from local file headers
394
+ const results: ParsedEntry[] = [];
395
+ for (const e of entries) {
396
+ const { value: lfh, offset: afterHeader } = readStruct(
397
+ view,
398
+ e.localHeaderOffset,
399
+ SCHEMA.LocalFile
400
+ );
401
+ ensureSig(lfh.signature, SIG.LocalFile, "Local File");
402
+ if (lfh.flags !== 0x0800)
403
+ throw new Error("Unsupported: flags (expect UTF-8 only)");
404
+ if (lfh.method !== 0)
405
+ throw new Error("Unsupported: compression (expect store only)");
406
+ if (lfh.extraLen !== 0)
407
+ throw new Error("Unsupported: extra fields in local header");
408
+
409
+ const dataStart = afterHeader;
410
+ const dataEnd = dataStart + lfh.compSize;
411
+ const data = e.isDirectory
412
+ ? new Uint8Array(0)
413
+ : new Uint8Array(u8.subarray(dataStart, dataEnd));
414
+
415
+ results.push({ ...e, data });
416
+ }
417
+
418
+ return results;
419
+ }
420
+
421
+ // Quick utility: return a map of file data (directories omitted)
422
+ static extract(input: ArrayBuffer | Uint8Array): Record<string, Uint8Array> {
423
+ const entries = this.parse(input);
424
+ const out: Record<string, Uint8Array> = {};
425
+ for (const e of entries) if (!e.isDirectory) out[e.name] = e.data;
426
+ return out;
427
+ }
428
+ }
429
+
430
+ // -------------------------
431
+ // Reader for TinyZip archives (our subset only)
432
+ // -------------------------
433
+ export interface ParsedEntry {
434
+ name: string;
435
+ isDirectory: boolean;
436
+ crc32: number;
437
+ size: number; // uncompressed
438
+ compSize: number; // same as size for store
439
+ time: number;
440
+ dosDate: number;
441
+ localHeaderOffset: number;
442
+ data: Uint8Array; // empty for directories
443
+ }
444
+
445
+ function toU8(input: string | ArrayBuffer | Uint8Array): Uint8Array {
446
+ if (typeof input === "string") return utf8(input);
447
+ if (input instanceof Uint8Array) return input;
448
+ return new Uint8Array(input);
449
+ }
450
+
451
+ function findEOCDOffset(u8: Uint8Array): number {
452
+ const view = new DataView(u8.buffer, u8.byteOffset, u8.byteLength);
453
+ const minSize = 22; // EOCD without comment
454
+ if (u8.length >= minSize) {
455
+ const at = u8.length - minSize;
456
+ if (view.getUint32(at, true) === SIG.EOCD) return at;
457
+ }
458
+ // Fallback: scan last 1 KiB for signature
459
+ const start = Math.max(0, u8.length - 1024);
460
+ for (let i = u8.length - 4; i >= start; i--) {
461
+ if (view.getUint32(i, true) === SIG.EOCD) return i;
462
+ }
463
+ throw new Error("EOCD not found");
464
+ }
@@ -0,0 +1,16 @@
1
+ const CRC_TABLE: Uint32Array = (() => {
2
+ const t = new Uint32Array(256);
3
+ for (let i = 0; i < 256; i++) {
4
+ let c = i;
5
+ for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
6
+ t[i] = c >>> 0;
7
+ }
8
+ return t;
9
+ })();
10
+
11
+ export function crc32(u8: Uint8Array): number {
12
+ let c = 0xffffffff;
13
+ for (let i = 0; i < u8.length; i++)
14
+ c = CRC_TABLE[(c ^ u8[i]) & 0xff] ^ (c >>> 8);
15
+ return (c ^ 0xffffffff) >>> 0;
16
+ }
@@ -0,0 +1,117 @@
1
+ // -------------------------
2
+ // Declarative header schemas + (en|de)coders
3
+ // -------------------------
4
+ type ScalarType = "u8" | "u16" | "u32";
5
+
6
+ type LenFn = (ctx: Record<string, any>) => number;
7
+
8
+ // Typed schema helpers
9
+ type NumberField<Name extends string, Kind extends ScalarType> = {
10
+ name: Name;
11
+ type: Kind;
12
+ };
13
+
14
+ type BytesField<Name extends string> = {
15
+ name: Name;
16
+ type: "bytes";
17
+ length: number | LenFn;
18
+ };
19
+
20
+ type SchemaFieldConst<Name extends string = string> =
21
+ | NumberField<Name, ScalarType>
22
+ | BytesField<Name>;
23
+
24
+ type FieldValue<F> = F extends { type: "bytes" } ? Uint8Array : number;
25
+
26
+ type StructFromSchema<S extends readonly SchemaFieldConst<string>[]> = {
27
+ [F in S[number] as F["name"] & string]: FieldValue<F>;
28
+ };
29
+
30
+ export const defineSchema = <
31
+ const S extends readonly SchemaFieldConst<string>[],
32
+ >(
33
+ s: S
34
+ ) => s;
35
+ export const defineSchemas = <
36
+ const M extends Record<string, readonly SchemaFieldConst<string>[]>,
37
+ >(
38
+ m: M
39
+ ) => m;
40
+
41
+ export type SchemaStructs<
42
+ M extends Record<string, readonly SchemaFieldConst<string>[]>,
43
+ > = {
44
+ [K in keyof M]: StructFromSchema<M[K]>;
45
+ };
46
+
47
+ export function sizeofFieldSpec(
48
+ f: SchemaFieldConst<string>,
49
+ ctx: Record<string, any>
50
+ ): number {
51
+ if (f.type === "bytes")
52
+ return typeof f.length === "function" ? (f.length as LenFn)(ctx) : f.length;
53
+ return f.type === "u8" ? 1 : f.type === "u16" ? 2 : 4;
54
+ }
55
+
56
+ export function writeStruct<S extends readonly SchemaFieldConst<string>[]>(
57
+ schema: S,
58
+ values: StructFromSchema<S>
59
+ ): Uint8Array {
60
+ // compute total size (bytes fields may depend on previously set values)
61
+ let total = 0;
62
+ const ctx: Record<string, any> = { ...(values as any) };
63
+ for (const f of schema) total += sizeofFieldSpec(f, ctx);
64
+
65
+ const out = new Uint8Array(total);
66
+ const view = new DataView(out.buffer);
67
+ let o = 0;
68
+ const vals = values as unknown as Record<string, any>;
69
+ for (const f of schema) {
70
+ if (f.type === "bytes") {
71
+ const b: Uint8Array = vals[f.name];
72
+ out.set(b, o);
73
+ o += b.length;
74
+ continue;
75
+ }
76
+ const n = Number(vals[f.name]) >>> 0;
77
+ if (f.type === "u8") {
78
+ view.setUint8(o, n);
79
+ o += 1;
80
+ } else if (f.type === "u16") {
81
+ view.setUint16(o, n, true);
82
+ o += 2;
83
+ } else {
84
+ view.setUint32(o, n, true);
85
+ o += 4;
86
+ }
87
+ }
88
+ return out;
89
+ }
90
+
91
+ export function readStruct<S extends readonly SchemaFieldConst<string>[]>(
92
+ view: DataView,
93
+ offset: number,
94
+ schema: S
95
+ ): { value: StructFromSchema<S>; offset: number } {
96
+ const ctx: Record<string, any> = {};
97
+ let o = offset;
98
+ for (const f of schema) {
99
+ if (f.type === "bytes") {
100
+ const len =
101
+ typeof f.length === "function" ? (f.length as LenFn)(ctx) : f.length;
102
+ const u8 = new Uint8Array(view.buffer, view.byteOffset + o, len);
103
+ ctx[f.name] = new Uint8Array(u8); // copy for safety
104
+ o += len;
105
+ } else if (f.type === "u8") {
106
+ ctx[f.name] = view.getUint8(o);
107
+ o += 1;
108
+ } else if (f.type === "u16") {
109
+ ctx[f.name] = view.getUint16(o, true);
110
+ o += 2;
111
+ } else {
112
+ ctx[f.name] = view.getUint32(o, true);
113
+ o += 4;
114
+ }
115
+ }
116
+ return { value: ctx as StructFromSchema<S>, offset: o };
117
+ }
package/src/noyaApp.ts CHANGED
@@ -1,4 +1,5 @@
1
- /* eslint-disable no-restricted-globals */
1
+ "use client";
2
+
2
3
  import { findAllDefs } from "@noya-app/noya-schemas";
3
4
  import {
4
5
  createOrCastValue,
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import {
2
4
  GetAtPath,
3
5
  Observable,