@p10node/office-engine 0.1.1 → 0.2.0

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.
@@ -6222,7 +6222,7 @@ var Wa = (e, t) => `${e}:${t}`, Ga = class {
6222
6222
  let a = this.sheetIndex(n, this.cur);
6223
6223
  if (a < 0) return new I("#REF!");
6224
6224
  let o = Wa(a, r * Q + i);
6225
- return e.has(o) && this.compute(o, e, t), this.value(a, r, i);
6225
+ return t.has(o) ? new I("#REF!") : (e.has(o) && this.compute(o, e, t), this.value(a, r, i));
6226
6226
  },
6227
6227
  bounds: (e) => {
6228
6228
  let t = this.wb.sheetsData.get(this.sheetIndex(e, this.cur));
@@ -7479,6 +7479,7 @@ var zo = class {
7479
7479
  root;
7480
7480
  P;
7481
7481
  cache = /* @__PURE__ */ new Map();
7482
+ found = /* @__PURE__ */ new Map();
7482
7483
  changed = !1;
7483
7484
  constructor(e) {
7484
7485
  this.nodes = t(e), this.root = a(this.nodes), this.P = i(this.root.q);
@@ -7493,6 +7494,23 @@ var zo = class {
7493
7494
  let r = o(n).length;
7494
7495
  return u(n, "count", String(r)), this.changed = !0, r - 1;
7495
7496
  }
7497
+ lookup(e) {
7498
+ let t = this.found.get(e);
7499
+ if (!t) {
7500
+ let r = /* @__PURE__ */ new Map();
7501
+ o(s(this.root, e)).forEach((e, t) => {
7502
+ let i = n([e]);
7503
+ r.has(i) || r.set(i, t);
7504
+ }), this.found.set(e, t = r);
7505
+ }
7506
+ return t;
7507
+ }
7508
+ intern(e, t) {
7509
+ let r = n([t]), i = this.lookup(e), a = i.get(r);
7510
+ if (a !== void 0) return a;
7511
+ let o = this.append(e, t);
7512
+ return i.set(r, o), o;
7513
+ }
7496
7514
  numFmtId(e) {
7497
7515
  for (let [t, n] of Object.entries(qa)) if (n === e) return Number(t);
7498
7516
  let t = s(this.root, "numFmts"), n = 163;
@@ -7525,13 +7543,13 @@ var zo = class {
7525
7543
  "font"
7526
7544
  ].some((e) => e in n)) {
7527
7545
  let e = o(this.section("fonts"), "font"), t = f(e[Number(l(p, "fontId") ?? 0)] ?? e[0] ?? d(a + "font")), r = (e, n) => m(t, n, e, Mo);
7528
- n.b !== void 0 && r("b", n.b ? d(a + "b") : void 0), n.i !== void 0 && r("i", n.i ? d(a + "i") : void 0), n.strike !== void 0 && r("strike", n.strike ? d(a + "strike") : void 0), n.u !== void 0 && r("u", n.u ? d(a + "u") : void 0), n.sz !== void 0 && r("sz", d(a + "sz", { val: String(n.sz) })), "color" in n && r("color", n.color ? d(a + "color", { rgb: "FF" + n.color.replace("#", "").toUpperCase() }) : void 0), n.font && (r("name", d(a + "name", { val: n.font })), r("scheme", void 0)), u(p, "fontId", String(this.append("fonts", t))), u(p, "applyFont", "1");
7546
+ n.b !== void 0 && r("b", n.b ? d(a + "b") : void 0), n.i !== void 0 && r("i", n.i ? d(a + "i") : void 0), n.strike !== void 0 && r("strike", n.strike ? d(a + "strike") : void 0), n.u !== void 0 && r("u", n.u ? d(a + "u") : void 0), n.sz !== void 0 && r("sz", d(a + "sz", { val: String(n.sz) })), "color" in n && r("color", n.color ? d(a + "color", { rgb: "FF" + n.color.replace("#", "").toUpperCase() }) : void 0), n.font && (r("name", d(a + "name", { val: n.font })), r("scheme", void 0)), u(p, "fontId", String(this.intern("fonts", t))), u(p, "applyFont", "1");
7529
7547
  }
7530
7548
  if ("fill" in n) {
7531
7549
  if (!n.fill) u(p, "fillId", "0");
7532
7550
  else {
7533
7551
  let e = t(`<${a}fill><${a}patternFill patternType="solid"><${a}fgColor rgb="FF${n.fill.replace("#", "").toUpperCase()}"/><${a}bgColor indexed="64"/></${a}patternFill></${a}fill>`)[0];
7534
- u(p, "fillId", String(this.append("fills", e)));
7552
+ u(p, "fillId", String(this.intern("fills", e)));
7535
7553
  }
7536
7554
  u(p, "applyFill", "1");
7537
7555
  }
@@ -7553,7 +7571,7 @@ var zo = class {
7553
7571
  "top",
7554
7572
  "bottom"
7555
7573
  ]) t in e && m(r, e[t] ? d(a + t, { style: n.borderStyle ?? "thin" }, [d(a + "color", { rgb: "FF" + i })]) : d(a + t), t, c);
7556
- s(r, "diagonal") || m(r, d(a + "diagonal"), "diagonal", c), u(p, "borderId", String(this.append("borders", r))), u(p, "applyBorder", "1");
7574
+ s(r, "diagonal") || m(r, d(a + "diagonal"), "diagonal", c), u(p, "borderId", String(this.intern("borders", r))), u(p, "applyBorder", "1");
7557
7575
  }
7558
7576
  if ("align" in n || "wrap" in n || "valign" in n || "rotate" in n) {
7559
7577
  let e = f(s(p, "alignment") ?? d(a + "alignment"));
@@ -7568,7 +7586,7 @@ var zo = class {
7568
7586
  ]), u(p, "applyAlignment", "1");
7569
7587
  }
7570
7588
  n.numFmt !== void 0 && (u(p, "numFmtId", String(this.numFmtId(n.numFmt))), u(p, "applyNumberFormat", "1"));
7571
- let h = this.append("cellXfs", p);
7589
+ let h = this.intern("cellXfs", p);
7572
7590
  return this.cache.set(r, h), h;
7573
7591
  }
7574
7592
  xml() {
@@ -7591,6 +7609,8 @@ var zo = class {
7591
7609
  if (!e || r.length <= n) continue;
7592
7610
  let i = new Set(r.slice(n));
7593
7611
  e.c = e.c.filter((e) => !i.has(e)), u(e, "count", String(n));
7612
+ let a = this.found.get(t);
7613
+ if (a) for (let [e, t] of a) t >= n && a.delete(e);
7594
7614
  }
7595
7615
  for (let [t, n] of this.cache) n >= e.cellXfs && this.cache.delete(t);
7596
7616
  }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Server-side snapshots: replays shared history onto a document headlessly, exactly the way a
3
+ * collaborator applies it, so the result can be saved as the room's new base file.
4
+ *
5
+ * const client = await DocumentClient.open(snapshotFile, { worker: false })
6
+ * const snap = await compact(client, { from: snapshotMeta, batches })
7
+ * const file = await client.call('save') // store it, then hub.snapshot(room, snap)
8
+ */
9
+ import type { DocumentClient } from '../core/worker/client';
10
+ import type { Batch, Snapshot } from './protocol';
11
+ export interface CompactOptions {
12
+ /** the snapshot `client` was opened from (default: the original upload, seq 0) */
13
+ from?: Snapshot;
14
+ /** batches to apply: every batch after `from.seq`, in order (older ones are ignored) */
15
+ batches: Batch[];
16
+ /** canonical batches kept as the new snapshot's context (default 1000) */
17
+ window?: number;
18
+ }
19
+ /**
20
+ * Applies `batches` to `client` (opened on the file of snapshot `from`) and returns the snapshot it
21
+ * now holds. Save the document afterwards with `client.call('save')`.
22
+ */
23
+ export declare function compact(client: DocumentClient, opts: CompactOptions): Promise<Snapshot>;
@@ -5,26 +5,46 @@
5
5
  * const conn = hub.connect((msg) => socket.send(encode(msg)))
6
6
  * socket.onmessage = (e) => conn.receive(decode(e.data))
7
7
  * socket.onclose = () => conn.close()
8
+ *
9
+ * Snapshots (see `compact`) bound the history: register one with `hub.snapshot()` and new clients
10
+ * open its file and only replay the batches after it, while the hub drops older batches.
8
11
  */
9
- import { type Batch, type ClientMsg, type HubMsg, type PeerInfo } from './protocol';
12
+ import { type Batch, type ClientMsg, type Entry, type HubMsg, type PeerInfo, type Snapshot } from './protocol';
10
13
  export interface RoomState {
11
14
  id: string;
12
15
  seq: number;
16
+ /** every batch with seq > `start`, in order */
13
17
  log: Batch[];
18
+ /** batches older than this were dropped (after a snapshot) */
19
+ start: number;
14
20
  ids: Set<string>;
15
21
  peers: Map<number, {
16
22
  info: PeerInfo;
17
23
  send: (m: HubMsg) => void;
18
24
  }>;
19
25
  nextSite: number;
26
+ /** snapshots clients can join from: seq → context */
27
+ snaps: Map<number, Entry[]>;
28
+ }
29
+ /** A persisted room: its latest snapshot and the batches after it (and possibly some before). */
30
+ export interface RoomLog {
31
+ log: Batch[];
32
+ snapshot?: Snapshot & {
33
+ sites?: number;
34
+ };
20
35
  }
36
+ type Loaded = Batch[] | RoomLog | undefined;
21
37
  export interface HubOptions {
22
38
  /** called for every sequenced batch (persist it to rebuild rooms after a restart) */
23
39
  onBatch?: (room: string, batch: Batch) => void;
24
- /** rebuilds a room log (e.g. from storage) the first time the room is used */
25
- loadLog?: (room: string) => Batch[] | undefined;
40
+ /** rebuilds a room (e.g. from storage) the first time it is used */
41
+ loadLog?: (room: string) => Loaded | Promise<Loaded>;
26
42
  /** called when the last peer left a room */
27
43
  onIdle?: (room: string) => void;
44
+ /** batches kept below the latest snapshot for clients reconnecting from older states (default 1000) */
45
+ retain?: number;
46
+ /** most recent snapshots clients can still join from (default 3) */
47
+ keep?: number;
28
48
  }
29
49
  export interface HubConnection {
30
50
  receive(msg: ClientMsg): void;
@@ -33,9 +53,20 @@ export interface HubConnection {
33
53
  export declare class CollabHub {
34
54
  private opts;
35
55
  readonly rooms: Map<string, RoomState>;
56
+ private loading;
36
57
  constructor(opts?: HubOptions);
37
- room(id: string): RoomState;
58
+ /** the room, loaded through `loadLog` the first time */
59
+ room(id: string): Promise<RoomState>;
38
60
  /** forgets a room (its log and peers) — e.g. when a new base file is uploaded */
39
61
  reset(id: string): void;
62
+ /** drops an idle room from memory (it is loaded again on the next join); false while peers are connected */
63
+ unload(id: string): boolean;
64
+ /**
65
+ * Registers a snapshot of a loaded room (`compact` output, its file stored where clients can open
66
+ * it): clients may join from it, and batches older than `retain` below it are dropped.
67
+ * Returns false when the snapshot doesn't fit the room (e.g. it was reset meanwhile).
68
+ */
69
+ snapshot(id: string, snap: Snapshot): boolean;
40
70
  connect(send: (m: HubMsg) => void): HubConnection;
41
71
  }
72
+ export {};
@@ -19,6 +19,11 @@ export interface CollabOptions {
19
19
  /** shared document id */
20
20
  room: string;
21
21
  user: UserInfo;
22
+ /**
23
+ * Sequence number of the snapshot `src` was opened from (see `compact`); default 0, the original
24
+ * upload. The hub then only sends the batches after it.
25
+ */
26
+ base?: number;
22
27
  }
23
28
  export interface Collab {
24
29
  readonly session: CollabSession<unknown>;
@@ -36,8 +41,10 @@ export interface Collab {
36
41
  close(): void;
37
42
  }
38
43
  export declare function startCollab(client: DocumentClient, opts: CollabOptions): Collab;
39
- export { CollabHub, type HubOptions, type HubConnection } from './hub';
44
+ export { CollabHub, type HubOptions, type HubConnection, type RoomLog, type RoomState } from './hub';
45
+ export { compact, type CompactOptions } from './compact';
46
+ export { RoomStore, MemoryBlobStore, type BlobStore, type RoomHead, type RoomStoreOptions, type Version } from './store';
40
47
  export { CollabSession, type Adapter, type CollabStatus } from './session';
41
48
  export { MemoryTransport, WebSocketTransport, type Transport, type TransportStatus } from './transport';
42
- export type { Batch, ClientMsg, HubMsg, PeerInfo, UserInfo } from './protocol';
49
+ export type { Batch, ClientMsg, Entry, HubMsg, PeerInfo, Snapshot, UserInfo } from './protocol';
43
50
  export { encode, decode } from './codec';
@@ -28,15 +28,38 @@ export interface Batch<Op = unknown> {
28
28
  /** client generated id (dedupes resends after reconnects) */
29
29
  id: string;
30
30
  ops: Op[];
31
+ /** when the hub sequenced it (ms since epoch) */
32
+ time?: number;
33
+ /** author: user id, else name */
34
+ user?: string;
35
+ }
36
+ /** A batch the way every client applies it: its ops transformed against the concurrent batches sequenced before it. */
37
+ export interface Entry<Op = unknown> {
38
+ seq: number;
39
+ site: number;
40
+ ops: Op[];
41
+ }
42
+ /**
43
+ * The document materialized at `seq` (see `compact`): a file containing every batch up to `seq`,
44
+ * plus the canonical batches right before it (`ctx`) that clients joining from that file need to
45
+ * merge batches still based on older states.
46
+ */
47
+ export interface Snapshot<Op = unknown> {
48
+ seq: number;
49
+ ctx: Entry<Op>[];
31
50
  }
32
51
  export type ClientMsg =
33
- /** join a room; `have`/`site` on reconnect */
52
+ /**
53
+ * join a room; `have`/`site` on reconnect. `base`: on the first join, the snapshot the client
54
+ * opened (default 0, the original upload)
55
+ */
34
56
  {
35
57
  t: 'hello';
36
58
  room: string;
37
59
  user: UserInfo;
38
60
  have?: number;
39
61
  site?: number;
62
+ base?: number;
40
63
  } | {
41
64
  t: 'ops';
42
65
  id: string;
@@ -47,7 +70,10 @@ export type ClientMsg =
47
70
  data: unknown;
48
71
  };
49
72
  export type HubMsg =
50
- /** `log` holds every batch with seq > `from`; `reset` when the hub lost history newer than `have` */
73
+ /**
74
+ * `log` holds every batch with seq > `from`; `ctx` the joined snapshot's context (first join from a
75
+ * snapshot); `reset` when the hub can't continue from the client's state (history lost or dropped)
76
+ */
51
77
  {
52
78
  t: 'welcome';
53
79
  site: number;
@@ -56,6 +82,7 @@ export type HubMsg =
56
82
  log: Batch[];
57
83
  peers: PeerInfo[];
58
84
  reset?: boolean;
85
+ ctx?: Entry[];
59
86
  } | {
60
87
  t: 'batch';
61
88
  b: Batch;
@@ -14,7 +14,7 @@
14
14
  * to rebase, like deleting a sheet) are not applied locally until the hub has
15
15
  * sequenced them.
16
16
  */
17
- import type { PeerInfo, UserInfo } from './protocol';
17
+ import type { Entry, PeerInfo, UserInfo } from './protocol';
18
18
  import type { Transport } from './transport';
19
19
  export interface ApplyResult {
20
20
  /** whatever `revert` needs to undo the op exactly (local ops only) */
@@ -60,6 +60,8 @@ export interface SessionOptions {
60
60
  user: UserInfo;
61
61
  /** canonical batches kept to transform late batches (default 5000) */
62
62
  logWindow?: number;
63
+ /** sequence number the opened file already contains: a snapshot (default 0, the original upload) */
64
+ base?: number;
63
65
  }
64
66
  export declare class CollabSession<Op = unknown> {
65
67
  private transport;
@@ -94,6 +96,8 @@ export declare class CollabSession<Op = unknown> {
94
96
  push(op: Op, undo: unknown): void;
95
97
  setPresence(data: unknown): void;
96
98
  on<K extends keyof SessionEvents>(event: K, fn: (d: SessionEvents[K]) => void): () => void;
99
+ /** the last `limit` batches as applied here (the context of a snapshot taken at `rev`) */
100
+ context(limit?: number): Entry<Op>[];
97
101
  /** resolves once everything queued so far was processed */
98
102
  idle(): Promise<void>;
99
103
  close(): void;
@@ -0,0 +1,94 @@
1
+ import type { RoomLog } from './hub';
2
+ import type { Batch, Snapshot } from './protocol';
3
+ /** Minimal object storage. Keys use `/` separators. */
4
+ export interface BlobStore {
5
+ /** null when missing */
6
+ get(key: string): Promise<Uint8Array | null>;
7
+ put(key: string, data: Uint8Array): Promise<void>;
8
+ /** every key starting with `prefix` (any order) */
9
+ list(prefix: string): Promise<string[]>;
10
+ delete(key: string): Promise<void>;
11
+ }
12
+ export declare class MemoryBlobStore implements BlobStore {
13
+ readonly data: Map<string, Uint8Array<ArrayBufferLike>>;
14
+ get(key: string): Promise<Uint8Array<ArrayBufferLike> | null>;
15
+ put(key: string, data: Uint8Array): Promise<void>;
16
+ list(prefix: string): Promise<string[]>;
17
+ delete(key: string): Promise<void>;
18
+ }
19
+ export interface RoomHead {
20
+ /** changes with every upload: stale writes of an older upload are ignored */
21
+ epoch: string;
22
+ /** latest snapshot */
23
+ seq: number;
24
+ size: number;
25
+ name: string;
26
+ type: string;
27
+ time: number;
28
+ }
29
+ /** a stored snapshot */
30
+ export interface Version {
31
+ seq: number;
32
+ size: number;
33
+ time: number;
34
+ }
35
+ export interface RoomStoreOptions {
36
+ /** key prefix, e.g. `collab/` */
37
+ prefix?: string;
38
+ /** batches are buffered and written as one segment at most this often (ms, default 250) */
39
+ flushMs?: number;
40
+ /** snapshots kept per room (default: all — the version history); older ones and their batches are deleted */
41
+ keep?: number;
42
+ /** batches below the latest snapshot loaded for clients reconnecting from older states (default 1000) */
43
+ retain?: number;
44
+ /** a write failed (it is retried) */
45
+ onError?: (error: Error, room: string) => void;
46
+ }
47
+ type SnapMeta = Version & {
48
+ sites?: number;
49
+ };
50
+ export declare class RoomStore {
51
+ readonly blobs: BlobStore;
52
+ private opts;
53
+ private heads;
54
+ private buffers;
55
+ private queues;
56
+ constructor(blobs: BlobStore, opts?: RoomStoreOptions);
57
+ private dir;
58
+ private epochDir;
59
+ /** runs `fn` after every earlier operation on `room` */
60
+ private run;
61
+ private readHead;
62
+ /** sets a room's document (snapshot 0 of a new epoch) and forgets its history */
63
+ create(room: string, file: Uint8Array, info: {
64
+ name: string;
65
+ type: string;
66
+ }): Promise<RoomHead>;
67
+ /** latest snapshot of the room (null: no such room) */
68
+ head(room: string): Promise<RoomHead | null>;
69
+ /** the document of snapshot `seq` */
70
+ file(room: string, seq: number, epoch?: string): Promise<Uint8Array | null>;
71
+ /** snapshot `seq` with its merge context */
72
+ snapshot(room: string, seq: number, epoch?: string): Promise<(Snapshot & SnapMeta) | null>;
73
+ /** stored snapshots, oldest first */
74
+ versions(room: string, epoch?: string): Promise<Version[]>;
75
+ /** stored batches with `after` < seq ≤ `upTo` (flushed ones only), in order */
76
+ batches(room: string, after: number, upTo?: number, epoch?: string): Promise<Batch[]>;
77
+ /** hub `onBatch`: buffers the batch, written with the next segment */
78
+ append(room: string, batch: Batch): void;
79
+ /** writes buffered batches now (all rooms when `room` is omitted) */
80
+ flush(room?: string): Promise<void>;
81
+ private flushRoom;
82
+ /** hub `loadLog`: the latest snapshot and the batches after it (plus `retain` before it) */
83
+ load(room: string): Promise<RoomLog | undefined>;
84
+ /**
85
+ * Stores a snapshot taken from `base` (the head it was compacted from) and makes it the latest.
86
+ * Returns the new head, or null when the room changed meanwhile (new upload, newer snapshot).
87
+ */
88
+ save(room: string, base: RoomHead, snap: Snapshot & {
89
+ sites?: number;
90
+ }, file: Uint8Array): Promise<RoomHead | null>;
91
+ /** drops snapshots beyond `keep` and the segments no remaining snapshot needs */
92
+ private prune;
93
+ }
94
+ export {};
@@ -11,7 +11,7 @@ export declare function mount(el: HTMLElement, options: MountOptions): MountedVi
11
11
  * `<office-viewer src="/file.xlsx" style="height:600px"></office-viewer>`
12
12
  * Rendered inside a shadow root so host page CSS cannot leak in.
13
13
  * Attributes: src, name, editable, toolbar / thumbnails / save / download / file-menu / open ("false" to hide);
14
- * real-time collaboration: collab (hub WebSocket URL), room, user (display name).
14
+ * real-time collaboration: collab (hub WebSocket URL), room, user (display name), base (snapshot seq `src` was opened at).
15
15
  * Property: `file` (File/Blob/ArrayBuffer) for local data.
16
16
  */
17
17
  export declare class OfficeViewerElement extends HTMLElement {
@@ -0,0 +1,4 @@
1
+ /** px size of a CSS font size ('11pt', px number), `base` when unset */
2
+ export declare const fontPx: (v: string | number | undefined, base: number) => number;
3
+ /** Excel-like: shrink General decimals to fit, otherwise show ### when a number is too wide. */
4
+ export declare function fitNumber(text: string, width: number, px: number): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@p10node/office-engine",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Browser engine to view DOCX, XLSX, PPTX progressively (worker-based, range-loading, virtualized React views)",
5
5
  "type": "module",
6
6
  "license": "MIT",